From fc05977de07eea73a0a041c18d730ce45406384c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Toledo?= Date: Sun, 4 Jan 2026 18:05:01 -0300 Subject: [PATCH] feat: Add pfSense hybrid template with OpenVPN monitoring, enhance YAML validation, and remove old DHCP execution plan. --- agent_execution_plan_dhcp.md | 105 --------- templates_gold/documentation_pfsense_snmp.md | 100 --------- .../INSTRUCOES_AGENTE.txt | 50 +++++ .../agent_execution_plan_openvpn.md | 199 ++++++++++++++++++ .../files/openvpn-discovery.sh | 34 +++ .../files/userparameter_openvpn.conf | 16 ++ .../template_pfsense_hybrid_gold.yaml} | 0 .../template_pfsense_hybrid_gold_generated.md | 73 +++++++ validate_zabbix_template.py | 73 +++++++ 9 files changed, 445 insertions(+), 205 deletions(-) delete mode 100644 agent_execution_plan_dhcp.md delete mode 100644 templates_gold/documentation_pfsense_snmp.md create mode 100644 templates_gold/pfsense_hybrid_snmp_agent/INSTRUCOES_AGENTE.txt create mode 100644 templates_gold/pfsense_hybrid_snmp_agent/agent_execution_plan_openvpn.md create mode 100644 templates_gold/pfsense_hybrid_snmp_agent/files/openvpn-discovery.sh create mode 100644 templates_gold/pfsense_hybrid_snmp_agent/files/userparameter_openvpn.conf rename templates_gold/{template_app_pfsense_snmp.yaml => pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold.yaml} (100%) create mode 100644 templates_gold/pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold_generated.md diff --git a/agent_execution_plan_dhcp.md b/agent_execution_plan_dhcp.md deleted file mode 100644 index 22be67c..0000000 --- a/agent_execution_plan_dhcp.md +++ /dev/null @@ -1,105 +0,0 @@ -# Plano de Execução: Monitoramento DHCP Multi-Instância pfSense - -Este documento guia o agente de IA na implementação do monitoramento granular de DHCP no pfSense, conforme definido no plano de implementação. - -**Arquivo Alvo:** `templates_gold/template_app_pfsense_snmp.yaml` - ---- - -## 1. Enriquecimento da Coleta SNMP (Item) - -**Objetivo:** Adicionar a OID de parâmetros (`hrSWRunParameters`) ao item de coleta de software para permitir diferenciar processos `dhcpd`. - -- **Ação:** Localizar o item com `key: pfsense.sw.walk`. -- **Alteração:** Adicionar a OID `1.3.6.1.2.1.25.4.2.1.5` à lista `snmp_oid`. -- **Código Atual (Referência):** - ```yaml - snmp_oid: walk[1.3.6.1.2.1.25.4.2.1.2,1.3.6.1.2.1.25.4.2.1.7] - ``` -- **Código Novo:** - ```yaml - snmp_oid: walk[1.3.6.1.2.1.25.4.2.1.2,1.3.6.1.2.1.25.4.2.1.7,1.3.6.1.2.1.25.4.2.1.5] - ``` -- **Atualização do Preprocessing:** Adicionar o passo para mapear a nova OID para um nome JSON (ex: `hrSWRunParameters`). - ```yaml - - type: SNMP_WALK_TO_JSON - parameters: - ... - - hrSWRunParameters - - 1.3.6.1.2.1.25.4.2.1.5 - - '0' - ``` - -## 2. Implementação da Regra de Descoberta (LLD) - -**Objetivo:** Criar uma nova regra de descoberta para detectar instâncias individuais do `dhcpd`. - -- **Local:** Seção `discovery_rules`. -- **Nova Regra:** - ```yaml - - uuid: (gerar_novo_uuid_v4) - name: 'Descoberta de Processos DHCP' - type: DEPENDENT - key: pfsense.dhcp.discovery - delay: '0' - description: 'Descobre instâncias do DHCP Server (dhcpd) diferenciadas por parâmetros (ex: IPv4 vs IPv6).' - master_item: - key: pfsense.sw.walk - filter: - evaltype: AND - conditions: - - macro: '{#HR_SW_NAME}' - value: 'dhcpd' - formulaid: A - lld_macro_paths: - - lld_macro: '{#HR_SW_NAME}' - path: '$.hrSWRunName' - - lld_macro: '{#HR_SW_PARAMS}' - path: '$.hrSWRunParameters' - - lld_macro: '{#HR_SW_STATUS}' - path: '$.hrSWRunStatus' - ``` - -## 3. Protótipos de Itens e Triggers - -**Objetivo:** Monitorar o status de cada instância descoberta. - -- **Item Prototype:** - ```yaml - - uuid: (gerar_novo_uuid_v4) - name: 'Status do Processo DHCP: {#HR_SW_PARAMS}' - type: DEPENDENT - key: 'pfsense.dhcp.process.status[{#HR_SW_PARAMS}]' - delay: '0' - description: 'Status da instância DHCP rodando com argumentos: {#HR_SW_PARAMS}' - valuemap: - name: 'Services status' - preprocessing: - - type: JSONPATH - parameters: - - '$[?(@.hrSWRunName == "{#HR_SW_NAME}" && @.hrSWRunParameters == "{#HR_SW_PARAMS}")].hrSWRunStatus.first()' - master_item: - key: pfsense.sw.walk - tags: - - tag: component - value: application - ``` - -- **Trigger Prototype:** - ```yaml - - uuid: (gerar_novo_uuid_v4) - expression: 'last(/PFSense by SNMP/pfsense.dhcp.process.status[{#HR_SW_PARAMS}])=0' - name: '🚨 DHCP Parado: Instância {#HR_SW_PARAMS} não está rodando' - priority: HIGH - description: 'A instância do DHCP com parâmetros "{#HR_SW_PARAMS}" parou de responder.' - ``` - -## 4. Validação Automática (Mandatório) - -**Objetivo:** Garantir que o template resultante seja válido e siga padrões (UUIDs únicos, etc). - -- **Comando:** - ```powershell - python validate_zabbix_template.py templates_gold/template_app_pfsense_snmp.yaml - ``` -- **Critério de Sucesso:** O script deve retornar `[SUCCESS] YAML Structure & UUIDs are VALID.` sem erros fatais. diff --git a/templates_gold/documentation_pfsense_snmp.md b/templates_gold/documentation_pfsense_snmp.md deleted file mode 100644 index 83011c2..0000000 --- a/templates_gold/documentation_pfsense_snmp.md +++ /dev/null @@ -1,100 +0,0 @@ -# Documentação: PFSense by SNMP - -**Template:** PFSense by SNMP -**Descrição:** -Template para monitoramento do pfSense via SNMP -Configuração: - 1. Habilite o daemon SNMP em Services na interface web do pfSense: https://docs.netgate.com/pfsense/en/latest/services/snmp.html - 2. Configure a regra de firewall para permitir acesso do Zabbix Proxy/Server via SNMP: https://docs.netgate.com/pfsense/en/latest/firewall/index.html#managing-firewall-rules - 3. Associe o template ao host. - -MIBs usadas: -BEGEMOT-PF-MIB -HOST-RESOURCES-MIB - -Gerado originalmente pela ferramenta oficial "Templator", Otimizado para Padrão Gold (Arthur) - -## Itens Monitorados - -### Itens Globais -- **Coleta Raw (SNMP): Interfaces de Rede PF** (`net.if.pf.walk`) -- **Coleta Raw (SNMP): Interfaces de Rede (IF-MIB)** (`net.if.walk`) -- **Status do servidor DHCP** (`pfsense.dhcpd.status`) -- **Status do servidor DNS** (`pfsense.dns.status`) -- **Estado do processo Nginx (Web)** (`pfsense.nginx.status`) -- **Pacotes com offset incorreto (Bad Offset)** (`pfsense.packets.bad.offset`) -- **Pacotes Fragmentados** (`pfsense.packets.fragment`) -- **Pacotes correspondentes a uma regra de filtro** (`pfsense.packets.match`) -- **Pacotes descartados por limite de memória** (`pfsense.packets.mem.drop`) -- **Pacotes Normalizados** (`pfsense.packets.normalize`) -- **Pacotes Curtos (Short Packets)** (`pfsense.packets.short`) -- **Status de execução do Packet Filter** (`pfsense.pf.status`) -- **Coleta Raw (SNMP): Contadores PF (pfCounter)** (`pfsense.pf_counters.walk`) -- **Contagem de regras de Firewall** (`pfsense.rules.count`) -- **Tabela de Rastreamento: Origens Atuais (Source Tracking)** (`pfsense.source.tracking.table.count`) -- **Tabela de Rastreamento: Limite (Limit)** (`pfsense.source.tracking.table.limit`) -- **Tabela de Rastreamento: Utilização (%)** (`pfsense.source.tracking.table.pused`) -- **Tabela de Estados: Atual (State Table)** (`pfsense.state.table.count`) -- **Tabela de Estados: Limite (Limit)** (`pfsense.state.table.limit`) -- **Tabela de Estados: Utilização (%)** (`pfsense.state.table.pused`) -- **Coleta Raw (SNMP): Software Instalado (hrSWRun)** (`pfsense.sw.walk`) -- **Disponibilidade do Agente SNMP** (`zabbix[host,snmp,available]`) - -### Regras de Descoberta (LLD) - -#### Descoberta de Processos DHCP (`pfsense.dhcp.discovery`) - - **Protótipos de Itens:** - - Processo DHCP [{#HR_SW_PARAMS}]: Status (`pfsense.dhcp.process.status[{#HR_SW_PARAMS}]`) - - **Protótipos de Triggers:** - - [HIGH] **🚨 DHCP Parado: Instância {#HR_SW_PARAMS} não está rodando** - -#### Descoberta de interfaces de rede (`pfsense.net.if.discovery`) - - **Protótipos de Itens:** - - Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de entrada bloqueado (`net.if.in.block.v4.bps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de entrada bloqueados (`net.if.in.block.v4.pps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de entrada bloqueado (`net.if.in.block.v6.bps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de entrada bloqueados (`net.if.in.block.v6.pps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes de entrada descartados (`net.if.in.discards[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes de entrada com erros (`net.if.in.errors[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de entrada permitido (`net.if.in.pass.v4.bps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de entrada permitidos (`net.if.in.pass.v4.pps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de entrada permitido (`net.if.in.pass.v6.bps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de entrada permitidos (`net.if.in.pass.v6.pps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Bits recebidos (`net.if.in[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de saída bloqueado (`net.if.out.block.v4.bps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de saída bloqueados (`net.if.out.block.v4.pps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de saída bloqueado (`net.if.out.block.v6.bps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes de saída descartados (`net.if.out.discards[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes de saída com erros (`net.if.out.errors[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv4 de saída permitido (`net.if.out.pass.v4.bps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv4 de saída permitidos (`net.if.out.pass.v4.pps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Tráfego IPv6 de saída permitido (`net.if.out.pass.v6.bps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Pacotes IPv6 de saída permitidos (`net.if.out.pass.v6.pps[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Bits enviados (`net.if.out[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Contagem de referências de regras (`net.if.rules.refs[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Velocidade (`net.if.speed[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Status operacional (`net.if.status[{#SNMPINDEX}]`) - - Interface [{#IFNAME}({#IFALIAS})]: Tipo de interface (`net.if.type[{#SNMPINDEX}]`) - -## Alertas (Triggers) - -### Triggers Globais -- [AVERAGE] **🚨 DHCP Parado: Servidor DHCP não está em execução** -- [AVERAGE] **🚨 DNS Parado: Servidor DNS (Unbound) não está em execução** -- [AVERAGE] **🚨 WebServer Parado: Nginx não está rodando** -- [WARNING] **🧩 Fragmentação Excessiva IPv4** -- [WARNING] **🛡️ Possível Ataque/Scan: Pico de Bloqueios** -- [HIGH] **🚨 Firewall Desligado: Packet Filter inativo** -- [WARNING] **⚠️ Tabela de Rastreamento Cheia: Uso elevado ({ITEM.LASTVALUE1}%)** -- [HIGH] **⏳ Source Tracking cheia em < 1h (Previsão)** -- [WARNING] **🔥 Tabela de Estados Crítica: Risco de bloqueio ({ITEM.LASTVALUE1}%)** -- [HIGH] **⏳ Tabela de Estados cheia em < 1h (Previsão)** -- [WARNING] **🚨 Falha SNMP: Sem coleta de dados no pfSense** - -### Protótipos de Triggers (LLD) - -**Regra: Descoberta de interfaces de rede** -- [WARNING] **🐢 Congestionamento: Descartes na interface {#IFNAME}** -- [WARNING] **⚠️ PFSense: Interface [{#IFNAME}({#IFALIAS})]: Alta taxa de erros de entrada** -- [WARNING] **⚠️ PFSense: Interface [{#IFNAME}({#IFALIAS})]: Alta taxa de erros de saída** -- [AVERAGE] **🔌 PFSense: Interface [{#IFNAME}({#IFALIAS})]: Link indisponível** diff --git a/templates_gold/pfsense_hybrid_snmp_agent/INSTRUCOES_AGENTE.txt b/templates_gold/pfsense_hybrid_snmp_agent/INSTRUCOES_AGENTE.txt new file mode 100644 index 0000000..f11585d --- /dev/null +++ b/templates_gold/pfsense_hybrid_snmp_agent/INSTRUCOES_AGENTE.txt @@ -0,0 +1,50 @@ +# INSTRUÇÕES DE INSTALAÇÃO - AGENTE ZABBIX P/ OPENVPN +===================================================== + +Para que o monitoramento do OpenVPN funcione no Template Hybrid Gold, você deve realizar os passos abaixo em CADA firewall pfSense. + +REQUISITOS +---------- +1. Acesso SSH ao pfSense (Opção 8 no console). +2. Pacote "Zabbix Agent" instalado no pfSense (System > Package Manager). + +PASSO 1: INSTALAR O SCRIPT DE DESCOBERTA +---------------------------------------- +Este script é usado pelo Zabbix para descobrir automaticamente os usuários conectados. + +1. Crie a pasta se não existir: + mkdir -p /opt/zabbix/ + +2. Copie o arquivo 'files/openvpn-discovery.sh' para '/opt/zabbix/openvpn-discovery.sh' no firewall. + (Você pode usar SCP ou criar o arquivo com 'vi' e colar o conteúdo). + +3. Dê permissão de execução: + chmod +x /opt/zabbix/openvpn-discovery.sh + +PASSO 2: CONFIGURAR O AGENTE +---------------------------- +Este arquivo ensina o Zabbix a usar o script acima e ler os logs. + +1. Copie o arquivo 'files/userparameter_openvpn.conf' para '/usr/local/etc/zabbix50/zabbix_agentd.conf.d/userparameter_openvpn.conf' + + ⚠️ NOTA: O caminho pode variar dependendo da versão do pacote Zabbix Agent (ex: zabbix60, zabbix40). Verifique com 'ls /usr/local/etc/'. + +PASSO 3: VERIFICAÇÃO DE CAMINHOS DE LOG +--------------------------------------- +O script assume que os logs de status do OpenVPN estão no padrão: + /var/log/openvpn/status*.log + +Se o seu pfSense estiver configurado para salvar em outro lugar (verifique em OpenVPN > Servers > Edit > Advanced Settings ou Logs), você DEVE editar os dois arquivos (.sh e .conf) e corrigir o caminho antes de instalar. + +PASSO 4: REINICIAR O SERVIÇO +---------------------------- +Após copiar os arquivos, reinicie o agente para aplicar as mudanças: + + service zabbix_agentd restart + +TESTE +----- +No terminal do pfSense, teste se o agente consegue ler a versão do OpenVPN: + zabbix_agentd -t openvpn.version + +Se retornar [t|2.x.x], está funcionando! diff --git a/templates_gold/pfsense_hybrid_snmp_agent/agent_execution_plan_openvpn.md b/templates_gold/pfsense_hybrid_snmp_agent/agent_execution_plan_openvpn.md new file mode 100644 index 0000000..1735714 --- /dev/null +++ b/templates_gold/pfsense_hybrid_snmp_agent/agent_execution_plan_openvpn.md @@ -0,0 +1,199 @@ +# AI Agent Execution Plan: OpenVPN Hybrid Integration + +**Objective**: transform the `pfsense_hybrid_snmp_agent` folder into a production-ready Hybrid solution. +**Context**: The files `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\files\openvpn-discovery.sh` and `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\files\userparameter_openvpn.conf` are currently raw copies. The YAML template is the original SNMP version located at `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\template_app_pfsense_snmp.yaml`. + + +## Execution Strategy & Safety Context +**Goal**: We are building a "Hybrid" Zabbix Template for pfSense that combines standard SNMP monitoring with advanced OpenVPN metrics collected via Zabbix Agent Custom UserParameters. +**Key Features**: +1. **Dynamic Grouping**: Group VPN users by "Company" (Server Name) derived from log filenames. +2. **Security Triggers**: Detect Exfiltration (>10GB/h), Zombie Sessions (>24h), and Session Hijacking (IP Change). +3. **S2S vs User**: Distinguish Site-to-Site tunnels from human users to apply different alert rules via Macros and Discovery Overrides. + +**CRITICAL INSTRUCTION**: If any step in this runbook is ambiguous, fails validation, or if you encounter unexpected file structures, **STOP IMMEDIATELY**. Do not guess. Ask the user for clarification before proceeding to the next step. + +--- + +## Phase 1: Script & Agent Configuration + +### 1.1. Rewrite `files/openvpn-discovery.sh` +**Path**: `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\files\openvpn-discovery.sh` +**Logic**: +- Scan `/var/log/openvpn/status*.log`. +- Extract `{#VPN.SERVER}` from filename (Regex: `status_(.*).log` -> Group 1). +- Extract Users, Real IP, Byte Counts from content. +- **Critical**: Output JSON standard for Zabbix LLD. + +```bash +#!/bin/sh +# OpenVPN Discovery Script (Arthur's Gold Standard) +# Outputs: {#VPN.USER}, {#VPN.SERVER}, {#VPN.REAL_IP} + +JSON_OUTPUT="{\"data\":[" +FIRST_ITEM=1 + +# Loop through all status logs +for logfile in /var/log/openvpn/status*.log; do + [ -e "$logfile" ] || continue + + # Extract Server Name from Filename "status_SERVERNAME.log" + # Note: Busybox filename parsing + filename=$(basename "$logfile") + # Remove prefix "status_" and suffix ".log" + server_name=$(echo "$filename" | sed -e 's/^status_//' -e 's/\.log$//') + + # Read the file and parse "CLIENT_LIST" lines + # Format: CLIENT_LIST,CommonName,RealAddress,VirtualAddress,BytesReceived,BytesSent,Since,Since(time_t),Username,ClientID,PeerID + while IFS=, read -r type common_name real_address virtual_address bytes_rx bytes_tx since since_unix username client_id peer_id; do + if [ "$type" = "CLIENT_LIST" ] && [ "$common_name" != "Common Name" ]; then + # Extract IP only from RealAddress (IP:PORT) + real_ip=$(echo "$real_address" | cut -d: -f1) + + # Append to JSON + if [ $FIRST_ITEM -eq 0 ]; then JSON_OUTPUT="$JSON_OUTPUT,"; fi + JSON_OUTPUT="$JSON_OUTPUT{\"{#VPN.USER}\":\"$common_name\",\"{#VPN.SERVER}\":\"$server_name\",\"{#VPN.REAL_IP}\":\"$real_ip\"}" + FIRST_ITEM=0 + fi + done < "$logfile" +done + +JSON_OUTPUT="$JSON_OUTPUT]}" +echo "$JSON_OUTPUT" +``` + +### 1.2. Update `files/userparameter_openvpn.conf` +**Path**: `C:\Users\joao.goncalves\Desktop\zabbix-itguys\templates_gold\pfsense_hybrid_snmp_agent\files\userparameter_openvpn.conf` +**Logic**: +- Simplify. The discovery script now does the heavy lifting of finding users. +- The Items need to fetch data. Since we have multiple files, `grep` needs to search ALL of them. +- **Optimization**: `grep -h` (no filename) to search all generic status logs. + +```conf +UserParameter=openvpn.discovery,/opt/zabbix/openvpn-discovery.sh +# Fetch raw metrics for a specific user (Usernames must be unique across servers or we grab the first match) +UserParameter=openvpn.user.bytes_received.total[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f6 +UserParameter=openvpn.user.bytes_sent.total[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f7 +UserParameter=openvpn.user.connected_since[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f9 +UserParameter=openvpn.user.real_address.new[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f3 | cut -d: -f1 +UserParameter=openvpn.user.status[*],if grep -q "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null; then echo 1; else echo 0; fi +UserParameter=openvpn.version,openvpn --version 2>&1 | head -1 | awk '{print $2}' +``` + +--- + +## Phase 2: Template Configuration (YAML) + +**Target**: `templates_gold/pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold.yaml` + +### 2.1. Add Macros +Append to `macros` section: +```yaml + - macro: '{$VPN.S2S.PATTERN}' + value: '^S2S_' + description: 'Regex para identificar túneis Site-to-Site.' + - macro: '{$VPN.DATA.LIMIT}' + value: '10737418240' + description: 'Limite de Download (10GB) para alerta de Exfiltração.' + - macro: '{$VPN.WORK.START}' + value: '080000' + - macro: '{$VPN.WORK.END}' + value: '180000' + - macro: '{$VPN.ZOMBIE.LIMIT}' + value: '86400' + description: 'Tempo máximo (24h) para considerar sessão zumbi.' +``` + +### 2.2. Add Discovery Rule +**Name**: `Descoberta de Usuários OpenVPN` +**Key**: `openvpn.discovery` +**Type**: `ZABBIX_ACTIVE` (Preferred for Agents behind NAT/Firewall) + +**Overrides**: +```yaml + overrides: + - name: 'Site-to-Site (S2S)' + step: '1' + filter: + conditions: + - macro: '{#VPN.USER}' + value: '{$VPN.S2S.PATTERN}' + operator: REGEXP + operations: + - operationobject: ITEM_PROTOTYPE + operator: REGEXP + value: 'Stats Year|Forecast' + status: ENABLED + - operationobject: TRIGGER_PROTOTYPE + operator: REGEXP + value: 'Exfiltração|Horário|Zombie|IP Change' + status: DISABLED + - operationobject: ITEM_PROTOTYPE + operator: LIKE + value: '' + tags: + - tag: Type + value: S2S + - name: 'User (Colaborador)' + step: '2' + filter: + conditions: + - macro: '{#VPN.USER}' + value: '{$VPN.S2S.PATTERN}' + operator: NOT_REGEXP + operations: + - operationobject: ITEM_PROTOTYPE + operator: REGEXP + value: 'Stats Year|Forecast' + status: DISABLED + - operationobject: TRIGGER_PROTOTYPE + operator: REGEXP + value: 'Exfiltração|Horário|Zombie|IP Change' + status: ENABLED + - operationobject: ITEM_PROTOTYPE + operator: LIKE + value: '' + tags: + - tag: Type + value: User +``` + +### 2.3. Add Item Prototypes (Selected) + +**Common Tags**: `Company: {{#VPN.SERVER}.regsub("(?:CLIENT_|S2S_)?(.*)", "\1")}` + +1. **Download Total (Raw)** + - Key: `openvpn.user.bytes_received.total[{#VPN.USER}]` + - Type: ZABBIX_ACTIVE + - Units: B + +2. **Download Rate (Calculated/Dependent?)** + - Actually, simpler to let Zabbix standard "Simple Change" preprocessing handle rate on a Dependent Item, or just use `Change per Second` preprocessing. + - **Decision**: Create `openvpn.user.bytes_received.rate[{#VPN.USER}]` dependent on Total, with `Change per Second`. + +3. **Real IP (Inventory)** + - Key: `openvpn.user.real_address.new[{#VPN.USER}]` + - Populates Host Inventory field? No, this is an item prototype. Just keep as value. + +**Reporting Metrics (Calculated)** +- **Stats Week**: `trendsum(//openvpn.user.bytes_received.total[{#VPN.USER}], 1w:now/w)` +- **Stats Month**: `trendsum(..., 1M:now/M)` +- **Forecast**: `(last(Month) / dayat()) * dayofmonth(end)` (Simplified) + +### 2.4. Add Triggers + +1. **Exfiltração**: `(last(Total) - last(Total, 1h)) > {$VPN.DATA.LIMIT}` +2. **IP Change**: `diff(RealIP)=1 and last(Status)=1` +3. **Zombie**: `last(ConnectedSince) < now() - {$VPN.ZOMBIE.LIMIT}` + +### 2.5. Add Dashboard +Insert `dashboards:` block at root (or modify existing if any). +- Use `type: svggraph` for Trends. +- Use `type: tophosts` for Tables (Columns: Name, Latest Value of Metric). + +--- + +## Phase 3: Validation + +1. **Lint**: `validate_zabbix_template.py`. +2. **Docs**: `generate_template_docs.py`. diff --git a/templates_gold/pfsense_hybrid_snmp_agent/files/openvpn-discovery.sh b/templates_gold/pfsense_hybrid_snmp_agent/files/openvpn-discovery.sh new file mode 100644 index 0000000..0ac74d2 --- /dev/null +++ b/templates_gold/pfsense_hybrid_snmp_agent/files/openvpn-discovery.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# OpenVPN Discovery Script (Arthur's Gold Standard) +# Outputs: {#VPN.USER}, {#VPN.SERVER}, {#VPN.REAL_IP} + +JSON_OUTPUT="{\"data\":[" +FIRST_ITEM=1 + +# Loop through all status logs +for logfile in /var/log/openvpn/status*.log; do + [ -e "$logfile" ] || continue + + # Extract Server Name from Filename "status_SERVERNAME.log" + # Note: Busybox filename parsing + filename=$(basename "$logfile") + # Remove prefix "status_" and suffix ".log" + server_name=$(echo "$filename" | sed -e 's/^status_//' -e 's/\.log$//') + + # Read the file and parse "CLIENT_LIST" lines + # Format: CLIENT_LIST,CommonName,RealAddress,VirtualAddress,BytesReceived,BytesSent,Since,Since(time_t),Username,ClientID,PeerID + while IFS=, read -r type common_name real_address virtual_address bytes_rx bytes_tx since since_unix username client_id peer_id; do + if [ "$type" = "CLIENT_LIST" ] && [ "$common_name" != "Common Name" ]; then + # Extract IP only from RealAddress (IP:PORT) + real_ip=$(echo "$real_address" | cut -d: -f1) + + # Append to JSON + if [ $FIRST_ITEM -eq 0 ]; then JSON_OUTPUT="$JSON_OUTPUT,"; fi + JSON_OUTPUT="$JSON_OUTPUT{\"{#VPN.USER}\":\"$common_name\",\"{#VPN.SERVER}\":\"$server_name\",\"{#VPN.REAL_IP}\":\"$real_ip\"}" + FIRST_ITEM=0 + fi + done < "$logfile" +done + +JSON_OUTPUT="$JSON_OUTPUT]}" +echo "$JSON_OUTPUT" \ No newline at end of file diff --git a/templates_gold/pfsense_hybrid_snmp_agent/files/userparameter_openvpn.conf b/templates_gold/pfsense_hybrid_snmp_agent/files/userparameter_openvpn.conf new file mode 100644 index 0000000..87ab9c1 --- /dev/null +++ b/templates_gold/pfsense_hybrid_snmp_agent/files/userparameter_openvpn.conf @@ -0,0 +1,16 @@ +# OpenVPN UserParameters for Zabbix Agent (Arthur's Gold Standard) +# Compatible with: Zabbix 7.0+ +# Installation: Copy to /usr/local/etc/zabbix72/zabbix_agentd.conf.d/ + +UserParameter=openvpn.discovery,/opt/zabbix/openvpn-discovery.sh + +# Fetch raw metrics for a specific user (Usernames must be unique across servers or we grab the first match) +UserParameter=openvpn.user.bytes_received.total[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f5 +UserParameter=openvpn.user.bytes_sent.total[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f6 +UserParameter=openvpn.user.connected_since[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f8 +UserParameter=openvpn.user.real_address.new[*],grep -h "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null | head -1 | cut -d, -f3 | cut -d: -f1 +UserParameter=openvpn.user.status[*],if grep -q "^CLIENT_LIST,$1," /var/log/openvpn/status*.log 2>/dev/null; then echo 1; else echo 0; fi + +# General OpenVPN Instance Metrics +UserParameter=openvpn.version,openvpn --version 2>&1 | head -1 | awk '{print $2}' +UserParameter=openvpn.user.count,grep -h "^CLIENT_LIST" /var/log/openvpn/status*.log 2>/dev/null | grep -v "Common Name" | wc -l \ No newline at end of file diff --git a/templates_gold/template_app_pfsense_snmp.yaml b/templates_gold/pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold.yaml similarity index 100% rename from templates_gold/template_app_pfsense_snmp.yaml rename to templates_gold/pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold.yaml diff --git a/templates_gold/pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold_generated.md b/templates_gold/pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold_generated.md new file mode 100644 index 0000000..897116a --- /dev/null +++ b/templates_gold/pfsense_hybrid_snmp_agent/template_pfsense_hybrid_gold_generated.md @@ -0,0 +1,73 @@ +# Documentação: PFSense Hybrid: SNMP + OpenVPN Agent + +**Template:** PFSense Hybrid: SNMP + OpenVPN Agent +**Descrição:** +Template Híbrido para monitoramento do pfSense. + +SNMP: Monitoramento de interfaces, firewall, serviços (DHCP, DNS, Nginx). +Agent: Monitoramento avançado de OpenVPN (usuários, túneis S2S, tráfego). + +Requisitos: + 1. Habilite o daemon SNMP em Services na interface web do pfSense. + 2. Instale o Zabbix Agent e configure os UserParameters OpenVPN (ver INSTRUCOES_AGENTE.txt). + 3. Associe o template ao host. + +MIBs: BEGEMOT-PF-MIB, HOST-RESOURCES-MIB +Keys Agent: openvpn.* + +Gerado pelo Padrão Gold (Arthur) - ITGuys + +## Itens Monitorados + +### Itens Globais +- **Coleta Raw (SNMP): Interfaces de Rede PF** (`net.if.pf.walk`) +- **Coleta Raw (SNMP): Interfaces de Rede (IF-MIB)** (`net.if.walk`) +- **Status do servidor DHCP** (`pfsense.dhcpd.status`) +- **Status do servidor DNS** (`pfsense.dns.status`) +- **Estado do processo Nginx (Web)** (`pfsense.nginx.status`) +- **Pacotes com offset incorreto (Bad Offset)** (`pfsense.packets.bad.offset`) +- **Pacotes Fragmentados** (`pfsense.packets.fragment`) +- **Pacotes correspondentes a uma regra de filtro** (`pfsense.packets.match`) +- **Pacotes descartados por limite de memória** (`pfsense.packets.mem.drop`) +- **Pacotes Normalizados** (`pfsense.packets.normalize`) +- **Pacotes Curtos (Short Packets)** (`pfsense.packets.short`) +- **Status de execução do Packet Filter** (`pfsense.pf.status`) +- **Coleta Raw (SNMP): Contadores PF (pfCounter)** (`pfsense.pf_counters.walk`) +- **Contagem de regras de Firewall** (`pfsense.rules.count`) +- **Tabela de Rastreamento: Origens Atuais (Source Tracking)** (`pfsense.source.tracking.table.count`) +- **Tabela de Rastreamento: Limite (Limit)** (`pfsense.source.tracking.table.limit`) +- **Tabela de Rastreamento: Utilização (%)** (`pfsense.source.tracking.table.pused`) +- **Tabela de Estados: Atual (State Table)** (`pfsense.state.table.count`) +- **Tabela de Estados: Limite (Limit)** (`pfsense.state.table.limit`) +- **Tabela de Estados: Utilização (%)** (`pfsense.state.table.pused`) +- **Coleta Raw (SNMP): Software Instalado (hrSWRun)** (`pfsense.sw.walk`) +- **Disponibilidade do Agente SNMP** (`zabbix[host,snmp,available]`) + +### Regras de Descoberta (LLD) + +#### Descoberta de Usuários OpenVPN (`openvpn.discovery`) + - **Protótipos de Itens:** + - OpenVPN [{#VPN.USER}]: Download Total (Bytes) (`openvpn.user.bytes_received.total[{#VPN.USER}]`) + - OpenVPN [{#VPN.USER}]: Upload Total (Bytes) (`openvpn.user.bytes_sent.total[{#VPN.USER}]`) + - OpenVPN [{#VPN.USER}]: IP Real (`openvpn.user.real_address.new[{#VPN.USER}]`) + - OpenVPN [{#VPN.USER}]: Status (`openvpn.user.status[{#VPN.USER}]`) + - OpenVPN [{#VPN.USER}]: Tempo Conectado (Unix) (`openvpn.user.connected_since[{#VPN.USER}]`) + +## Alertas (Triggers) + +### Triggers Globais +- [AVERAGE] **🚨 DHCP Parado: Servidor DHCP não está em execução** +- [AVERAGE] **🚨 DNS Parado: Servidor DNS (Unbound) não está em execução** +- [AVERAGE] **🚨 WebServer Parado: Nginx não está rodando** +- [WARNING] **🧩 Fragmentação Excessiva IPv4** +- [WARNING] **🛡️ Possível Ataque/Scan: Pico de Bloqueios** +- [HIGH] **🚨 Firewall Desligado: Packet Filter inativo** +- [WARNING] **⚠️ Tabela de Rastreamento Cheia: Uso elevado ({ITEM.LASTVALUE1}%)** +- [HIGH] **⏳ Source Tracking cheia em < 1h (Previsão)** +- [WARNING] **🔥 Tabela de Estados Crítica: Risco de bloqueio ({ITEM.LASTVALUE1}%)** +- [HIGH] **⏳ Tabela de Estados cheia em < 1h (Previsão)** +- [WARNING] **🚨 Falha SNMP: Sem coleta de dados no pfSense** + +### Protótipos de Triggers (LLD) + +**Regra: Descoberta de Usuários OpenVPN** diff --git a/validate_zabbix_template.py b/validate_zabbix_template.py index 7e03d17..bad3a61 100644 --- a/validate_zabbix_template.py +++ b/validate_zabbix_template.py @@ -190,10 +190,83 @@ def validate_dashboard_references(content, graph_names): return errors +def check_duplicate_yaml_keys(file_path): + """ + Check for duplicate YAML keys at the same level (e.g., two 'macros:' sections). + This is a Zabbix import killer - YAML parsers silently merge, but Zabbix rejects. + Uses regex-based scanning since yaml.safe_load silently handles duplicates. + Returns list of errors. + + Note: Only checks for duplicates at template-level (indent 4) since nested + keys like 'triggers:' can legitimately appear multiple times in different + item contexts. + """ + errors = [] + + try: + with open(file_path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except Exception as e: + errors.append(f"[FILE ERROR] Could not read file: {e}") + return errors + + # Track keys at template level (indent 4) only + # Key: key_name -> list of line numbers + template_level_keys = {} + + # Critical keys that should never be duplicated at template level + critical_keys = {'macros', 'items', 'discovery_rules', 'dashboards', + 'graphs', 'valuemaps', 'value_maps'} + + for line_num, line in enumerate(lines, 1): + # Skip comments and empty lines + stripped = line.lstrip() + if not stripped or stripped.startswith('#'): + continue + + # Calculate indentation (spaces before content) + indent = len(line) - len(line.lstrip()) + + # Only check template-level keys (indent 4 for " macros:") + if indent != 4: + continue + + # Match YAML key pattern: "key:" or "key: value" + import re + match = re.match(r'^(\s*)([a-zA-Z_][a-zA-Z0-9_]*):', line) + if match: + key_name = match.group(2) + + # Only track critical keys + if key_name in critical_keys: + if key_name not in template_level_keys: + template_level_keys[key_name] = [] + template_level_keys[key_name].append(line_num) + + # Report duplicates + for key_name, line_numbers in template_level_keys.items(): + if len(line_numbers) > 1: + lines_str = ', '.join(map(str, line_numbers)) + errors.append(f"[DUPLICATE KEY] '{key_name}:' appears {len(line_numbers)} times at template level (lines: {lines_str})") + + return errors + + def validate_yaml(file_path): print(f"Validating {file_path}...") print("=" * 60) + # ========== 0. Check for duplicate YAML keys (pre-parse) ========== + print("\n[0/5] Checking for duplicate YAML keys...") + duplicate_key_errors = check_duplicate_yaml_keys(file_path) + if duplicate_key_errors: + print(f" ❌ Found {len(duplicate_key_errors)} duplicate key issues") + for err in duplicate_key_errors: + print(f" ❌ {err}") + return False + else: + print(" ✅ No duplicate YAML keys detected") + try: with open(file_path, 'r', encoding='utf-8') as f: content = yaml.safe_load(f)