feat(wordpress): EMCP Tools workflow skills + widget catalogs + pending wordpress skills

- emcp-page-building, emcp-content-ops, emcp-site-audit: EMCP Tools MCP
  workflows verified live (atomic/legacy interplay, apply-template overwrite
  risk, create-theme-template goes live immediately, false-positive malware
  pattern in scan-security, change ledger + rollback)
- elementor-pro-widgets: 30+5 native Elementor Pro widgets (curated catalog)
- elementskit-widgets / powerpack-widgets: 42 + 97 third-party widgets,
  widgetType extracted from plugin source (not guessed by convention)
- plugin.json bumped 1.2.0 -> 1.3.0, keywords + description updated
- commits pending wordpress skills already present as untracked files
  (emcp-tools, wordfence, wp-activity-log, seguranca-descomplicar,
  webp-express, wp-fastest-cache, wp-font-perf, wp-meteor,
  wp-activity-log, redis-object-cache, app-for-cloudflare) and pending
  edits (rank-math, wp-cli, wp-content-seo-gate)
This commit is contained in:
Claude Code
2026-08-19 03:44:00 +01:00
parent aa981e8089
commit 4a55d51329
22 changed files with 6432 additions and 29 deletions
+738
View File
@@ -0,0 +1,738 @@
---
name: wordfence
description: Auditoria e gestão completa do Wordfence Security via SQL directo em servidores CWP — cobre as 313 chaves de `wfconfig`, a tabela separada `wfls_settings` do módulo Login Security (2FA/passkeys/CAPTCHA/XML-RPC), armazenamento do WAF em ficheiros (`wp-content/wflogs/config*.php`), WAF (modos, categorias de regra, blacklist premium), bloqueio de IPs/países/padrões (UA/referrer/hostname), Live Traffic, os 5 limitadores de Rate Limiting, Diagnostics, Import/Export de config e Wordfence Central. Detecção de IP real atrás do Cloudflare (howGetIPs), scan settings e interacção com plugins de cache. Usar quando "wordfence", "waf wordpress", "firewall wordpress", "2fa wordpress", "passkeys wordpress", "bloqueio de login", "wfconfig", "wfls_settings", "cloudflare real ip wordfence", "howGetIPs", "scan malware wordpress", "brute force wordpress", "rate limiting wordpress", "live traffic wordfence", "wordfence central", "bloqueio de país wordpress".
---
# /wordfence — Auditoria e Configuração Wordfence via SQL
Gestão do Wordfence Security via `wp db query` directo no servidor CWP.
**A configuração NÃO vive toda no mesmo sítio:**
- Grosso da config (WAF nível aplicação, scan, alertas, blocking,
rate limiting) → tabela própria `wfconfig` (chave/valor, colunas
`name`/`val`), **não** `wp_options` — `wp option get/patch` não
funciona para nada disto.
- 2FA, passkeys, CAPTCHA de login, XML-RPC, "remember device" (módulo
Login Security) → tabela **separada** `wfls_settings` (colunas
`name`/`value` — nomes de coluna diferentes de `wfconfig`). Ver §6.
- Config do motor WAF usada para arrancar antes do WordPress (Extended
Protection/auto-prepend) → **ficheiros PHP serializados** em
`wp-content/wflogs/config*.php`, fora de qualquer tabela SQL,
sincronizados automaticamente a partir de `wfconfig`. Ver §10.
**Fonte:** `CONFIG-Plugins-Referencia.md` §8 (Wordfence, mapeado
16-08-2026, site piloto `emanuelalmeida.pt`, versão `9.0.0` gratuita) +
leitura completa do código-fonte do plugin (466 ficheiros PHP) e
verificação SQL/SSH ao vivo feitas nesta sessão — detalhe completo da
cobertura no rodapé "Fonte" no final do documento.
---
## Contexto CWP — sempre obrigatório
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH) # ex.: wpne_, wpah_ — nunca assumir wp_
```
**Acesso via SSH:** `server.descomplicar.pt`, porta `9443`, user `root`.
Servidor executa comandos como o utilizador do site: `sudo -u USER wp ... --path=$PATH`.
**Tabela de config real (schema confirmado):**
```
DESCRIBE ${PREFIX}wfconfig;
name varchar(100) PRI
val longblob -- valor (string, número ou JSON serializado)
autoload enum('no','yes')
```
Não há `wp option patch` equivalente — é sempre `SELECT`/`UPDATE` directo
sobre `name`/`val`. **313 linhas** de config no site piloto.
---
## Decision tree — qual comando usar
| Operação | Comando |
|---|---|
| Ler 1 chave de config | `wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='...';"` |
| Ler várias chaves | `SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN (...);` |
| Alterar 1 chave (produção) | `UPDATE ${PREFIX}wfconfig SET val='...' WHERE name='...';` — **preferir sempre a UI do wp-admin** para chaves que disparam hooks (`wordfence_changed_ip_source`, `wordfence_changed_license_key`) |
| Contar utilizadores com 2FA | `SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets;` (tabela moderna) — **não** `usermeta` (legado, pode ficar desactualizado) |
| Ver todas as tabelas do plugin | `SHOW TABLES LIKE '${PREFIX}wf%';` |
| Ver issues de scan pendentes | `SELECT COUNT(*) FROM ${PREFIX}wfpendingissues;` |
| Ver IPs bloqueados pelo WAF | `SELECT COUNT(*) FROM ${PREFIX}wfblockediplog;` |
| Confirmar recomendação de proxy do próprio Wordfence | `SELECT val FROM ${PREFIX}wfconfig WHERE name='detectProxyRecommendation';` |
| Ler settings do módulo Login Security (2FA/passkeys/CAPTCHA) | `SELECT name, value FROM ${PREFIX}wfls_settings ORDER BY name;` — **tabela diferente**, colunas `name`/`value` (não `val`) — ver §6 |
| Ver ficheiros de config do WAF (bootstrap fora do WordPress) | `cat wp-content/wflogs/config.php` / `config-synced.php` — **não é SQL**, é PHP serializado com `__halt_compiler()` — ver §10 |
---
## 1. Auditoria de estado (leitura, sem risco)
```bash
PATH=/home/USER/public_html
PREFIX=$(wp db prefix --allow-root --path=$PATH)
# Estado geral do WAF/firewall
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('firewallEnabled','waf_status','disableWAFIPBlocking','isPaid');" --allow-root --path=$PATH
# Login security (rate-limit + 2FA)
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('loginSec_maxFailures','loginSec_countFailMins','loginSec_lockoutMins',
'loginSec_lockInvalidUsers','allowLegacy2FA','loginSec_requireAdminTwoFactor');" --allow-root --path=$PATH
# Quantos utilizadores TÊM 2FA activo de facto (tabela moderna do módulo login-security)
wp db query "SELECT COUNT(*) AS total_2fa FROM ${PREFIX}wfls_2fa_secrets;" --allow-root --path=$PATH
# Detecção de IP real (crítico atrás de proxy/CDN)
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('howGetIPs','howGetIPs_trusted_proxies','howGetIPs_trusted_proxy_preset',
'detectProxyRecommendation');" --allow-root --path=$PATH
# Scans activos
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'scansEnabled_%';" --allow-root --path=$PATH
# Alertas/email
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('alertEmails','email_summary_enabled','liveTrafficEnabled','blockFakeBots','autoUpdate','cacheType');" --allow-root --path=$PATH
# Volume de actividade (contexto do site)
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfblockediplog;" --allow-root --path=$PATH # IPs bloqueados pelo WAF
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfpendingissues;" --allow-root --path=$PATH # issues de scan em aberto
wp db query "SHOW TABLES LIKE '${PREFIX}wf%';" --allow-root --path=$PATH # inventário de tabelas do plugin
```
### Valores reais confirmados (`emanuelalmeida.pt`, gratuito, 16-08-2026)
| Chave | Valor | Avaliação |
|---|---|---|
| `firewallEnabled` | `1` | ✅ |
| `waf_status` | `enabled` | ✅ WAF a funcionar |
| `disableWAFIPBlocking` | `0` | ✅ bloqueio de IP activo (325 IPs bloqueados no log) |
| `isPaid` | vazio | Versão **gratuita** — sem WAF em tempo real (updates de regras com atraso), sem scan de reputação de IP premium |
| `alertEmails` | `it@descomplicar.pt` | ✅ |
| `email_summary_enabled` | `1` | ✅ |
| `liveTrafficEnabled` | `1` | ✅ |
| `blockFakeBots` | `1` | ✅ bloqueia falsos Googlebot/etc. |
| `autoUpdate` | `1` | ✅ |
| `cacheType` | `disabled` | ✅ **deliberado** — ver §4 |
| `scansEnabled_core/plugins/themes/malware/diskSpace/suspiciousAdminUsers/wafStatus/...` | todos `1` | ✅ cobertura de scan quase completa |
| `scansEnabled_highSense` | `0` | Sensibilidade alta de scan desligada — reduz falsos positivos, aceitável |
| `scansEnabled_scanImages` | `0` | Não faz scan binário a imagens — aceitável, baixo risco típico |
| `scansEnabled_checkHowGetIPs` | `1` | O próprio Wordfence **já tem um scan check dedicado a esta má-configuração** (ver §3) |
| `loginSec_maxFailures` / `_countFailMins` / `_lockoutMins` | `5` / `60` / `240` | Bloqueia ao fim de 5 falhas em 60 min, lockout de 4h |
| `loginSec_lockInvalidUsers` | `0` | Não bloqueia por tentar logins com username inexistente (só falhas de password em conta real) |
| `allowLegacy2FA` | `0` | ✅ não permite métodos 2FA fracos/antigos |
---
## 2. GAP real — 2FA não forçado, zero utilizadores protegidos
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
# → 0 = 2FA NÃO é obrigatório para administradores
wp db query "SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets;" --allow-root --path=$PATH
# → 0 = zero utilizadores com 2FA activo, incluindo o admin
```
**Achado real (`emanuelalmeida.pt`):** `loginSec_requireAdminTwoFactor=0` e
**zero** linhas em `wfls_2fa_secrets` — o login continua protegido só por
password + rate-limit (5 tentativas/60min/lockout 4h), sem segundo factor,
mesmo sendo o site pessoal do fundador da empresa.
### Forçar 2FA para administradores
**Auditoria/leitura sempre segura.** Escrita é mutação em produção —
confirmar autorização antes de aplicar:
```bash
# Ler estado actual antes de alterar
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
# Forçar 2FA para o role Administrator
wp db query "UPDATE ${PREFIX}wfconfig SET val='1' WHERE name='loginSec_requireAdminTwoFactor';" --allow-root --path=$PATH
```
**Cuidado operacional:** activar isto **sem** um administrador já ter um
autenticador configurado bloqueia o próprio acesso ao wp-admin no login
seguinte (o Wordfence força a configuração do 2FA no momento do login, não
tranca por fora — mas confirmar sempre com o utilizador antes, e nunca
aplicar em sessão headless sem acesso de recuperação garantido, ex. acesso
SSH/SQL directo para reverter o valor para `0` se algo correr mal).
---
## 3. GAP real — `howGetIPs` vazio atrás do Cloudflare
**Este é o achado mais concreto desta auditoria.** O próprio Wordfence já
tinha calculado a recomendação correcta e guardado numa chave separada, mas
**nunca a aplicou** à chave activa:
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name IN
('howGetIPs','detectProxyRecommendation');" --allow-root --path=$PATH
```
```
name val
detectProxyRecommendation HTTP_CF_CONNECTING_IP
howGetIPs (vazio)
```
`detectProxyRecommendation` é preenchido automaticamente pelo Wordfence
quando detecta que o tráfego chega via um proxy conhecido (Cloudflare, neste
caso) — mas **fica só como sugestão**; `howGetIPs` (a chave que o WAF
realmente usa para decidir qual IP é o "real") continua vazia. Com
`howGetIPs` vazio, o Wordfence cai no fallback de ordem de headers
(`lib/wfUtils.php`: `HTTP_CF_CONNECTING_IP` → `HTTP_X_REAL_IP` →
`REMOTE_ADDR` → `HTTP_X_FORWARDED_FOR`), o que **na maioria dos casos até
funciona**, mas não é o comportamento explícito/auditável e não activa o
scan check dedicado (`scansEnabled_checkHowGetIPs=1` já está ligado — é
precisamente este scan que devia estar a assinalar isto como issue, mas
`wfpendingissues` está vazia, i.e. o scan periódico pode não ter corrido
recentemente ou o alerta expirou).
### Valores válidos de `howGetIPs` (confirmado no código, `wfConfig.php`/`wfUtils.php`)
Cabeçalhos HTTP reconhecidos pelo Wordfence para determinar o IP real:
| Valor | Quando usar |
|---|---|
| `HTTP_CF_CONNECTING_IP` | Site atrás de **Cloudflare** — este é o caso do bundle Descomplicar® (todos os sites usam App for Cloudflare®) |
| `HTTP_X_REAL_IP` | Proxy tipo nginx reverso simples (sem CDN) |
| `HTTP_X_FORWARDED_FOR` | Load balancer/proxy genérico |
| (vazio) | `REMOTE_ADDR` directo — só correcto se não houver proxy/CDN nenhum à frente |
Existe ainda `howGetIPs_trusted_proxy_preset`, que aponta para uma entrada
em `ipResolutionList` (JSON de ~8KB, sincronizado da API Wordfence, contém
blocos de IP de CDNs conhecidas — ex. `cloudflare`, `cloudfront` — usados
para validar que o pedido realmente vem de um proxy confiável antes de
aceitar o header). Consultar presets disponíveis:
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='ipResolutionList';" --allow-root --path=$PATH \
| python3 -c "import json,sys; print(list(json.load(sys.stdin).keys()))"
```
### Corrigir (copiar a própria recomendação do Wordfence)
```bash
# Ler antes de escrever
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='detectProxyRecommendation';" --allow-root --path=$PATH
# Aplicar — usa o valor que o próprio Wordfence já recomendou
wp db query "UPDATE ${PREFIX}wfconfig SET val='HTTP_CF_CONNECTING_IP' WHERE name='howGetIPs';" --allow-root --path=$PATH
# Confirmar
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name='howGetIPs';" --allow-root --path=$PATH
```
Sem isto configurado, o rate-limiting de login, os bloqueios de IP do WAF e
o Live Traffic podem estar a agir sobre o IP do proxy Cloudflare em vez do
IP real do atacante — mais grave em sites com muito tráfego atrás do mesmo
edge, onde o "IP" observado pode ser partilhado por múltiplos visitantes.
---
## 4. Interacção com cache de página — `cacheType=disabled` é deliberado
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='cacheType';" --allow-root --path=$PATH
# → disabled
```
O Wordfence tem o seu próprio sistema de cache de página interno
(`Enable Falcon Engine`), mas em sites do bundle Descomplicar® **a cache de
página já é feita por outro plugin dedicado** (WP Fastest Cache ou
equivalente). `cacheType=disabled` está **correcto** — dois sistemas de
cache HTML activos ao mesmo tempo competem, servem versões desalinhadas e
dificultam o purge coordenado. **Nunca activar o cache Falcon do Wordfence
num site que já tem WP Fastest Cache/WP Meteor a fazer cache de página** —
confirmar sempre qual plugin de cache está activo antes de alterar
`cacheType` num site novo.
---
## 5. Scan settings — cobertura completa
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'scansEnabled_%';" --allow-root --path=$PATH
```
| Categoria | Chave | Estado (piloto) |
|---|---|---|
| Core WordPress | `scansEnabled_core`, `_coreUnknown`, `_oldVersions` | `1` |
| Plugins/temas | `scansEnabled_plugins`, `_themes` | `1` |
| Malware/ficheiros suspeitos | `scansEnabled_malware`, `_fileContents`, `_fileContentsGSB`, `_suspectedFiles` | `1` |
| Espaço em disco | `scansEnabled_diskSpace` | `1` |
| Utilizadores admin suspeitos | `scansEnabled_suspiciousAdminUsers` | `1` |
| Opções de BD suspeitas | `scansEnabled_options`, `_suspiciousOptions` | `1` |
| Passwords fracas | `scansEnabled_passwds` | `1` |
| Comentários/posts (spam/malware injectado) | `scansEnabled_comments`, `_posts` | `1` |
| GeoIP / Google Safe Browsing | `scansEnabled_geoipSupport`, `_checkGSB` | `1` |
| Config legível externamente | `scansEnabled_checkReadableConfig` | `1` |
| **Detecção de IP mal configurada** | `scansEnabled_checkHowGetIPs` | `1` — ver §3 |
| Estado do WAF | `scansEnabled_wafStatus` | `1` |
| WPScan (directory listing, full path disclosure) | `scansEnabled_wpscan_directoryListingEnabled`, `_wpscan_fullPathDisclosure` | `1` |
| Sensibilidade alta | `scansEnabled_highSense` | `0` (desligado, reduz falsos positivos) |
| Scan binário de imagens | `scansEnabled_scanImages` | `0` (desligado) |
**Avaliação:** cobertura de scan quase total (23 de 25 checks activos),
única omissão relevante é a sensibilidade alta — aceitável para reduzir
ruído em sites com actividade legítima elevada.
---
## 6. Two-Factor / Passkeys / CAPTCHA — tabela SEPARADA `wfls_settings`
**Achado arquitectural importante desta expansão:** o módulo "Login
Security" (2FA, passkeys, CAPTCHA de login, XML-RPC, "remember device",
integração WooCommerce, NTP) **não guarda a sua configuração em
`wfconfig`** — vive numa tabela própria, `wfls_settings`, gerida pela
classe `WordfenceLS\Controller_Settings` (código em
`modules/login-security/classes/controller/settings.php`). Schema:
```
DESCRIBE ${PREFIX}wfls_settings;
name varchar(191) PRI
value longtext -- nota: coluna chama-se "value", não "val"
autoload enum('no','yes')
```
```bash
wp db query "SELECT name, value FROM ${PREFIX}wfls_settings ORDER BY name;" --allow-root --path=$PATH
```
As chaves `loginSec_requireAdminTwoFactor` e `loginSec_enableSeparateTwoFactor`
em `wfconfig` (§2) são a versão **legada** — um switch global único. A
versão actual do plugin é **baseada em papéis (roles)**:
`migrate_admin_2fa_requirements_to_roles()` migra o switch legado para
chaves por role (`required-2fa-role.<role>`) dentro de `wfls_settings` e
depois apaga a chave legada do `wfconfig`. Um site "antigo" pode ainda ter
o valor legado; um site já migrado tem-no ausente/zero e a fonte de
verdade passa a ser `wfls_settings` + capabilities.
### Chaves confirmadas em `wfls_settings` (`emanuelalmeida.pt`)
| Chave | Valor confirmado | O que controla |
|---|---|---|
| `xmlrpc-enabled` | `1` | Exigir 2FA também em pedidos XML-RPC autenticados |
| `allow-xml-rpc` | `1` | Permitir XML-RPC de todo (independente do 2FA) |
| `ip-source` | vazio (`automatic`) | Fonte do IP real para o módulo login-security — **independente** de `howGetIPs` do `wfconfig` (§3); valores: `automatic`, `REMOTE_ADDR`, `X-Forwarded-For`, `X-Real-IP` |
| `ip-trusted-proxies` | vazio | Lista de proxies confiáveis (newline-separated) para o cálculo de IP deste módulo |
| `2fa-user-grace-period` | `10` | Dias de período de graça depois de 2FA se tornar obrigatório por role, antes de bloquear o login |
| `require-2fa.administrator` | vazio | Legado (ver acima); use `required-2fa-role.administrator` |
| `passkey-sign-count-mode` | `reject-lower` | Política anti-clonagem de passkeys: `allow` / `reject-lower` / `reject-lower-and-zero` |
| `passkey-relying-party-override` | vazio | Override do hostname RP para passkeys (multi-domínio) |
| `remember-device` | vazio (`false`) | "Lembrar este dispositivo" para saltar 2FA X dias |
| `remember-device-duration` | `2592000` (30 dias) | Duração do "remember device" em segundos |
| `always-show-login-security-menu` | `1` | Mostrar sempre o menu Login Security mesmo sem nada configurado |
| `enable-auth-captcha` | vazio | CAPTCHA (reCAPTCHA) no formulário de login |
| `recaptcha-site-key` / `recaptcha-secret` | vazio | Credenciais reCAPTCHA — só relevantes se `enable-auth-captcha=1` |
| `recaptcha-threshold` | `0.5` | Limiar de pontuação do reCAPTCHA v3 (0–1) |
| `enable-woocommerce-integration` / `-account-integration` | vazio | Aplicar 2FA/passkeys também na área de conta WooCommerce |
| `use-ntp` | `1` | Sincronizar relógio via NTP para códigos TOTP (evita drift do relógio do servidor) |
| `whitelisted` | vazio | IPs isentos de exigência de 2FA (não confundir com `whitelisted` do `wfconfig`, que é do WAF) |
| `schema-version` | `3` | Versão do schema interno do módulo — não editar manualmente |
### Modelo de permissões por role (capabilities, não settings)
"2FA obrigatório" e "2FA opcional" por role **não** são uma chave de
configuração simples — são geridos como **capabilities** do WordPress,
sincronizadas por `WordfenceLS\Controller_Permissions`:
| Capability | Significado |
|---|---|
| `wf2fa_activate_2fa_self` | O utilizador pode activar/desactivar 2FA na própria conta |
| `wf2fa_activate_2fa_others` | Pode activar/desactivar 2FA de outros utilizadores |
| `wf2fa_manage_settings` | Pode editar as definições do módulo Login Security |
| `wfls_manage_passkey_self` / `_others` | Equivalente para passkeys |
| `wfls_show_login_security` | Capability interna que mostra/esconde o menu (sincronizada automaticamente) |
```bash
wp eval 'var_export(get_role("administrator")->capabilities);' --allow-root --path=$PATH | grep -i 'wf2fa\|wfls'
# Confirmado em emanuelalmeida.pt: administrator TEM wf2fa_activate_2fa_self (pode activar),
# mas isso é OPCIONAL — só se torna obrigatório se required-2fa-role.administrator tiver timestamp > -1
```
O estado "obrigatório desde quando" fica em `required-2fa-role.<role>`
(timestamp Unix) ou `-1` (não obrigatório) — chave dinâmica dentro de
`wfls_settings`, não aparece na lista de defaults porque só existe depois
de alguém a definir pela UI.
### Tabelas de dados do módulo (não config, mas relevantes para auditoria)
| Tabela | Conteúdo |
|---|---|
| `wfls_2fa_secrets` | Segredos TOTP activos por utilizador (contagem = utilizadores com 2FA activo) |
| `wfls_passkeys` | Passkeys (WebAuthn) registadas por utilizador |
| `wfls_role_counts` | Cache de contagens de utilizadores por role, usada pela UI |
---
## 7. Live Traffic — configuração completa
Todas as chaves vivem em `wfconfig` (ao contrário do módulo 2FA). Tabela
de dados: `wflivetraffichuman` (visitas humanas) + `wfhits`, `wfhoover`,
`wfcrawlers`, `wflocs`, `wflogins` (dados de suporte ao Live Traffic e ao
dashboard de tráfego).
| Chave | Valor (piloto) | O que controla |
|---|---|---|
| `liveTrafficEnabled` | `1` | Liga/desliga o registo de Live Traffic |
| `liveTraf_ignorePublishers` | `1` | Não regista visitas de utilizadores autenticados com capacidade de publicar (reduz ruído do próprio staff) |
| `liveTraf_ignoreUsers` | vazio | Lista de usernames (separados por vírgula) a ignorar |
| `liveTraf_ignoreIPs` | vazio | Lista de IPs (separados por vírgula) a ignorar |
| `liveTraf_ignoreUA` | vazio | User-agent a ignorar (substring) |
| `liveTraf_maxRows` | `2000` | Quantidade máxima de registos guardados |
| `liveTraf_maxAge` | `30` | Dias máximos de retenção dos registos |
| `liveTraf_displayExpandedRecords` | `0` | Preferência de UI (registos expandidos por omissão) — não afecta captura |
**Overrides que desligam Live Traffic independentemente da config:**
`wfConfig::liveTrafficEnabled()` força `false` se a constante
`WORDFENCE_DISABLE_LIVE_TRAFFIC` estiver definida, ou se
`WF_IS_WP_ENGINE` for verdadeiro (hosts WP Engine desactivam sempre este
módulo, independentemente do valor em `wfconfig`) — útil para explicar a
um cliente porque "liguei e não aparece nada" nesse tipo de hosting.
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'liveTraf%' OR name='liveTrafficEnabled';" --allow-root --path=$PATH
```
---
## 8. Blocking completo — País, IP manual, Padrões (UA/Referrer/Hostname)
Todos os bloqueios (manuais e automáticos) vivem numa única tabela,
`wfBlocks7` (não em `wfconfig`), com um campo `type` que distingue a
natureza do bloqueio (`models/block/wfBlock.php`):
| Constante | Valor | Origem |
|---|---|---|
| `TYPE_IP_MANUAL` | 1 | Bloqueio de IP manual pelo admin |
| `TYPE_WFSN_TEMPORARY` | 2 | Resposta automática da Wordfence Security Network (rede partilhada entre sites) |
| `TYPE_COUNTRY` | 3 | Bloqueio por país |
| `TYPE_PATTERN` | 4 | Bloqueio avançado (IP range + hostname + user-agent + referrer) |
| `TYPE_RATE_BLOCK` | 5 | Gerado pelo rate limiting (§9), acção `block` |
| `TYPE_RATE_THROTTLE` | 6 | Gerado pelo rate limiting (§9), acção `throttle` |
| `TYPE_LOCKOUT` | 7 | Lockout de login (falhas de password/2FA) |
| `TYPE_IP_AUTOMATIC_TEMPORARY` / `_PERMANENT` | 8 / 9 | Bloqueios automáticos do WAF, temporário vs promovido a permanente por acção do admin |
```bash
# Ver todos os bloqueios activos por tipo
wp db query "SELECT type, COUNT(*) FROM ${PREFIX}wfblocks7 WHERE expiration=0 OR expiration>UNIX_TIMESTAMP() GROUP BY type;" --allow-root --path=$PATH
```
### Bloqueio por país
Campos guardados em `parameters` (JSON): `blockLogin` (bloquear só a
página de login), `blockSite` (bloquear o resto do site), `countries`
(array de códigos ISO). Comportamento configurado globalmente em
`wfconfig`:
| Chave | Valor (piloto) | O que controla |
|---|---|---|
| `cbl_action` | `block` | O que fazer a um país bloqueado: `block` (503) ou `redirect` (redireccionar) |
| `cbl_redirURL` | vazio | URL de destino se `cbl_action=redirect` |
| `cbl_loggedInBlocked` | `0` | Bloquear país mesmo que o visitante já tenha sessão iniciada |
| `cbl_bypassRedirURL` / `cbl_bypassRedirDest` | vazio | Um visitante de país bloqueado que aceda a este URL relativo é reencaminhado e recebe um cookie de bypass permanente |
| `cbl_bypassViewURL` | vazio | Um visitante permitido que veja este URL relativo recebe o cookie de bypass antecipadamente (para o caso de mudar de país/VPN depois) |
| `cbl_cookieVal` | gerado automaticamente | Valor único do cookie de bypass — regenerar invalida bypasses concedidos |
Nenhum destes está configurado no piloto (site sem bloqueio de país
activo) — confirmar sempre `hasCountryBlock()`/`countryBlocks()` antes de
assumir que não há impacto em visitantes internacionais.
### Bloqueio manual de IP / IP range
```bash
# Confirmar que um IP não está na whitelist antes de o bloquear (a UI faz isto automaticamente)
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='whitelisted';" --allow-root --path=$PATH
```
`wfBlock::isWhitelisted()` verifica dois níveis: a lista manual em
`whitelisted` (`wfconfig`, uma entrada por linha, IP/CIDR) e uma lista de
serviços conhecidos (`whitelistedServices`, JSON — vazio `{}` no piloto;
quando preenchido guarda ranges de IP de serviços como monitorização
externa que o Wordfence nunca bloqueia mesmo com regras activas).
### Bloqueio avançado (Custom Pattern) — hostname / user-agent / referrer
Bloqueio combinável por 4 critérios (pelo menos 1 obrigatório),
`type=custom-pattern` na validação (`wfBlock::validate()`):
| Campo | Formato | Exemplo de uso |
|---|---|---|
| `ipRange` | IP único, CIDR, ou range `1.2.3.4-1.2.3.9` (IPv4 e IPv6 não podem misturar-se no mesmo bloqueio) | Bloquear uma subnet inteira |
| `hostname` | wildcard `*` e `.` (regex `^[a-z0-9\.\*\-]+$`) | Bloquear por reverse-DNS de um datacenter conhecido |
| `userAgent` | substring livre | Bloquear scrapers com UA identificável |
| `referrer` | substring livre | Bloquear tráfego de referrer spam |
```bash
wp db query "SELECT id, blockedTime, reason, expiration FROM ${PREFIX}wfblocks7 WHERE type=4 AND (expiration=0 OR expiration>UNIX_TIMESTAMP());" --allow-root --path=$PATH
```
### Como o Wordfence decide não bloquear crawlers do Google
```bash
wp db query "SELECT val FROM ${PREFIX}wfconfig WHERE name='neverBlockBG';" --allow-root --path=$PATH
# Confirmado no piloto: neverBlockVerified
```
| Valor de `neverBlockBG` | Comportamento |
|---|---|
| `neverBlockVerified` (default, activo no piloto) | Verifica UA **e** faz PTR/reverse-DNS lookup para confirmar que é mesmo um IP do Google antes de isentar de bloqueio |
| `neverBlockUA` | Confia só no User-Agent, sem verificação de IP — menos seguro (fácil de fingir), mas mais barato |
| `treatAsOtherCrawlers` | Não dá tratamento especial a "Google" — pode ser bloqueado como qualquer outro crawler |
---
## 9. Rate Limiting completo — 5 tipos, `throttle` vs `block`
`models/block/wfRateLimit.php` define **5 limitadores independentes**,
cada um com um par de chaves `<nome>` (limiar) + `<nome>_action`
(`throttle` ou `block`). `DISABLED` como valor do limiar desactiva esse
limitador especificamente.
| Limitador | Chave limiar | Chave acção | Valor confirmado (piloto) | Aplica-se a |
|---|---|---|---|---|
| Global | `maxGlobalRequests` | `maxGlobalRequests_action` | `240` / `throttle` | **Qualquer** pedido, humano ou crawler — se activo, sobrepõe-se aos outros 4 |
| Page views de crawlers | `maxRequestsCrawlers` | `maxRequestsCrawlers_action` | `DISABLED` / `throttle` | Só User-Agents identificados como crawler (`wfCrawl::isCrawler`) |
| 404s de crawlers | `max404Crawlers` | `max404Crawlers_action` | `60` / `throttle` | Idem, mas só páginas 404 |
| Page views de humanos | `maxRequestsHumans` | `maxRequestsHumans_action` | `DISABLED` / `throttle` | Visitantes não identificados como crawler |
| 404s de humanos | `max404Humans` | `max404Humans_action` | `60` / `throttle` | Idem, mas só páginas 404 |
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'max%Requests%' OR name LIKE 'max404%' OR name='blockedTime';" --allow-root --path=$PATH
```
**No piloto, o rate limit global (`maxGlobalRequests=240`) está
ACTIVO** — qualquer IP (humano ou bot) que exceda 240 pedidos na janela
de 1 minuto (tabela `wfTrafficRates`, campo `eMin`) é afectado. Os
limitadores dedicados a crawlers/humanos de page views estão desligados;
só os de 404 (60 num minuto) estão activos, o que é uma configuração
razoável (protege contra scans de 404 sem penalizar navegação normal).
**`throttle` vs `block` não são só rótulos — têm durações e severidade
diferentes:**
- `throttle` → `wfBlock::createRateThrottle()`, duração **fixa de 60
segundos** (`wfRateLimit::rateLimitThrottleDuration()`) — resposta 503
temporária, o visitante volta a conseguir aceder ao fim de 1 minuto.
- `block` → `wfBlock::createRateBlock()`, duração = `blockedTime`
(`wfconfig`, **300s / 5 min no piloto**, configurável) — bloqueio mais
longo, e pode ser promovido a permanente manualmente pelo admin na UI
de Blocking (`TYPE_RATE_BLOCK` → `TYPE_IP_AUTOMATIC_PERMANENT`).
**Excepção comum a 404s permitidos:** `allowed404s` (`wfconfig`, uma
entrada por linha, suporta wildcard `*`) — URLs que nunca contam para os
limitadores de 404, mesmo que devolvam 404 de facto. Default do piloto:
`/favicon.ico`, `/apple-touch-icon*.png`, `/*@2x.png`,
`/browserconfig.xml` (ícones/manifestos pedidos por browsers que geram
404 legítimos e não devem disparar rate limiting).
---
## 10. WAF — arquitectura de armazenamento, modos e categorias de regra
**Achado arquitectural crítico:** para o WAF funcionar *antes* do
WordPress carregar (Extended Protection / auto-prepend), o Wordfence
**não pode depender só da base de dados** — mantém uma cópia da config
relevante em ficheiros PHP no filesystem, fora de `wp_content/plugins`:
```bash
ls -la wp-content/wflogs/
# config.php — config nuclear do WAF (wafStatus, authKey, versão do formato) — 6 chaves só
# config-synced.php — MIRROR de chaves relevantes do wfconfig + bloqueios activos, sincronizado
# sempre que algo muda (país/IP/pattern blocks, howGetIPs, apiKey, etc.)
# config-livewaf.php — config activa/em uso pelo processo WAF autoprepended
# config-transient.php— cache de dados de ataque (grande, ~1.7 MB no piloto)
# rules.php — ficheiro PHP executável (não serializado) com as regras do WAF, incluído
# directamente no bootstrap; NÃO editar/apagar manualmente
# attack-data.php, ips.php — logs internos de ataques/IPs vistos pelo WAF
```
Estes ficheiros começam com `<?php exit('Access denied');
__halt_compiler(); ?>` seguido de dados serializados em PHP — só são
legíveis fazendo `substr()` a partir do offset de `__halt_compiler()` e
`unserialize()`. **Nunca editar estes ficheiros manualmente**; qualquer
alteração de config deve passar pela UI ou por `wfconfig`, que depois é
sincronizada automaticamente para estes ficheiros (via
`wfWAFIPBlocksController::setNeedsSynchronizeConfigSettings()`, chamado
sempre que `wfBlock::create*()` corre).
O motor de armazenamento é escolhido por `WFWAF_STORAGE_ENGINE`
(`waf/bootstrap.php`): **`file`** por omissão (confirmado no piloto —
ficheiros em `wflogs/` existem e têm conteúdo recente), ou **`mysqli`**
forçado automaticamente em hosts WP Engine/Flywheel (ou por constante
explícita), caso em que a config do WAF passa a viver numa ligação MySQL
directa (bypass ao `$wpdb`) em vez de ficheiros.
### Modos e níveis do WAF (`models/firewall/wfFirewall.php`)
| Constante | Valores possíveis | Significado |
|---|---|---|
| `firewallMode()` | `disabled` / `learning-mode` / `enabled` | Estado geral; **`enabled`** no piloto (§1, `waf_status`) |
| `protectionMode()` | `basic` / `extended` | `extended` requer `WFWAF_AUTO_PREPEND` activo (ficheiro `.user.ini`/`.htaccess` a fazer `auto_prepend_file` para `wordfence-waf.php` antes do WordPress arrancar) — sem isto, o WAF só corre depois do `muplugins_loaded`, mais tarde no ciclo, "Basic Protection" |
| `ruleMode()` | `community` / `premium` | Depende de `isPaid` — piloto usa feed `community` (regras com atraso de propagação face a sites Premium) |
| `blacklistMode()` | `disabled` / `enabled` | "Real-Time IP Blocklist" — **só disponível com `isPaid`**, mesmo que `disableWAFBlacklistBlocking=0` |
| `learningModeStatus()` | `false` / `true` / timestamp | Se em Learning Mode, indica se há mudança automática agendada para Enabled (`learningModeGracePeriodEnabled` + `learningModeGracePeriod`) |
### Categorias de regra (fail-score) — confirmado no `rules.php` do piloto
O motor de regras usa 3 categorias de ataque com pontuação de risco
própria (`$this->failScores[...]` em `wflogs/rules.php`):
```bash
ssh server "grep -o \"failScores\\['[a-z]*'\\]\" .../wp-content/wflogs/rules.php | sort -u"
# failScores['rce'] — Remote Code Execution
# failScores['sqli'] — SQL Injection
# failScores['xss'] — Cross-Site Scripting
```
Regras individuais podem ser desactivadas via `disabledRules` — essa
chave vive **dentro do storage do WAF** (`wp-content/wflogs/config.php`
no motor `file`), não em `wfconfig`; o piloto não tem nenhuma regra
desactivada (chave ausente = todas activas). A "pontuação de segurança"
mostrada no Dashboard (`wfFirewall::overallStatus()`) combina: % de
regras activas (35%), blacklist premium activa (35%), Extended Protection
(20%), Rate Limiting/Advanced Blocking ligado (10%) — pesado a favor do
WAF (80% do total), com brute-force protection a contar só 20%.
---
## 11. Diagnostics (Tools → Diagnostics)
Gerado por `lib/wfDiagnostic.php`, sem persistência em BD — é sempre
calculado on-demand quando a página carrega. Secções e o que cada uma
verifica:
| Secção | O que reporta |
|---|---|
| Wordfence Status | Versão instalada, versão da base GeoIP, jobs de cron em atraso (>30min) |
| Filesystem | Se o webserver consegue ler/escrever `wp-content/plugins/wordfence` e `wp-content/wflogs` |
| Wordfence Config | Teste de escrita/leitura real contra `wfconfig` (básico e serializado) — falha aqui = sem permissões de escrita na tabela |
| Wordfence Firewall | Auto-prepend activo, motor de storage configurado vs activo, path dos logs, permissões de ficheiros, conteúdo de `.htaccess`/`.user.ini` relacionado com o WAF |
| MySQL | Versão do MySQL + privilégios reais do utilizador da BD: `SELECT`/`INSERT`/`UPDATE`/`DELETE`/`CREATE`/`ALTER`/`DROP`/`TRUNCATE` (via `SHOW GRANTS FOR current_user()`) |
| PHP Environment | Versão PHP vs mínimo suportado, dono do processo PHP, suporte OpenSSL/cURL (versões, protocolos), `display_errors` |
| Connectivity | Ligação de saída a servidores Wordfence (https), ligação de volta ao próprio site (IPv4 e IPv6 — útil para detectar Cloudflare a bloquear o próprio site) |
| Time | Hora da rede Wordfence, hora do servidor, offset NTP, fonte de tempo usada para TOTP (2FA), fuso horário do WordPress |
**Casos de uso práticos:**
- `userCanAlter`/`userCanDrop` a `false` → migrações/upgrades do plugin
podem falhar silenciosamente; verificar antes de reportar "scan não
actualiza issues".
- `connectToSelf`/`connectToSelfIpv6` a falhar com sinais de Cloudflare
(`cf-mitigated: challenge`, `Cloudflare Ray ID` no body) → o próprio
Cloudflare está a desafiar o loopback request do Wordfence; relevante
para sites do bundle Descomplicar® (todos atrás de Cloudflare via
App for Cloudflare®) em diagnóstico de scans que nunca terminam.
- A secção "WordPress" (via `wfDiagnostic::getWordpressValues()`, não
listada acima por ser genérica ao core) expõe constantes como
`WP_DEBUG`, `DISALLOW_FILE_EDIT`, `WP_CACHE`, `FORCE_SSL_ADMIN` — útil
para confirmar hardening sem abrir `wp-config.php` directamente.
---
## 12. Tools → Import/Export de configuração
**Não é um ficheiro local** (ao contrário de outros plugins do bundle) —
`wfImportExportController` (`lib/wfImportExportController.php`) faz
`export()`/`import()` **através da API do wordfence.com**: o export
gera um token remoto (`export_options`), que depois se cola manualmente
no site de destino (`import_options` + token). Isto exige que ambos os
sites tenham `apiKey` válida e liguem para fora ao wordfence.com — não
funciona offline nem é um download/upload de ficheiro local.
O que é exportado: todas as `wfConfig::getExportableOptionsKeys()`
(essencialmente os grupos `checkboxes` + `otherParams` do
`$defaultConfig`, não o grupo `defaultsOnly`), o agendamento de scans
(`scanSched`, serializado), e todos os registos da tabela de bloqueios
(`wfBlock::exportBlocks()` — inclui bloqueios de país, IP e pattern, mas
não lockouts nem bloqueios automáticos temporários).
**Não é adequado para "clonar config para outro site" via SQL directo**
(copiar a tabela `wfconfig` entre sites não é suportado oficialmente e
arrasta chaves específicas da instalação como `encKey`/`longEncKey` —
preferir sempre o fluxo de token oficial acima ou reconfigurar via UI).
---
## 13. Wordfence Central — gestão remota multi-site
```bash
wp db query "SELECT name, val FROM ${PREFIX}wfconfig WHERE name LIKE 'wordfenceCentral%';" --allow-root --path=$PATH
```
**Achado confirmado no piloto: este site ESTÁ ligado ao Wordfence
Central** (`wordfenceCentralConnected=1`), associado a
`it@descomplicar.pt` desde 27/02/2025
(`wordfenceCentralConnectTime=1740679066`), com um `wordfenceCentralSiteID`
próprio (UUID). Central é a consola de gestão remota multi-site do
Wordfence (central.wordfence.com) — permite monitorizar/alertar e (em
planos pagos) configurar Wordfence em vários sites a partir de um único
painel, autenticado via JWT (`wordfenceCentralAccessToken`,
`wordfenceCentralUserSiteAccessToken`) trocado através de um par de
chaves pública/privada gerado no site (`wordfenceCentralPK`,
`wordfenceCentralSecretKey`).
| Chave relevante | O que é |
|---|---|
| `wordfenceCentralConnected` | `1`/`0` — liga/desliga a integração |
| `wordfenceCentralConnectEmail` / `_ConnectTime` | Quem ligou e quando |
| `wordfenceCentralPluginAlertingDisabled` | Se `1`, silencia alertas duplicados no próprio wp-admin porque já chegam via Central |
| `wordfenceCentralConfigurationIssue` | Flag interna — `1` indica falha na renovação do token JWT, útil para diagnosticar "Central parou de reportar" |
| `wordfenceCentralSiteData` | Cache JSON da última resposta da API Central sobre este site |
**Widget do dashboard nativo do WordPress:**
`email_summary_dashboard_widget_enabled` (`wfconfig`, `1` no piloto)
controla o widget "Activity Report" no `/wp-admin/index.php` (WordPress
Dashboard nativo) — **não confundir** com o próprio Dashboard do
Wordfence (`/wp-admin/admin.php?page=Wordfence`, componente Vue sem
chave de config individual para mostrar/esconder secções).
---
## Gotchas / erros comuns
| Sintoma | Causa | Solução |
|---|---|---|
| `wp option get wordfence_...` devolve vazio/erro | Config do Wordfence não vive em `wp_options` | Usar sempre `wp db query` contra `${PREFIX}wfconfig` (colunas `name`/`val`) |
| `SELECT value FROM wfconfig` falha (`Unknown column 'value'`) | A coluna chama-se `val`, não `value` | `SELECT name, val FROM ${PREFIX}wfconfig` |
| Contagem de 2FA por `usermeta.meta_key='wordfence_2fa_secret'` parece desactualizada | Essa é a chave legada; o módulo login-security moderno guarda em tabela própria | Preferir `SELECT COUNT(*) FROM ${PREFIX}wfls_2fa_secrets` |
| `howGetIPs` vazio mas o site parece "funcionar" na mesma | O Wordfence tem fallback de ordem de headers (`HTTP_CF_CONNECTING_IP` → `HTTP_X_REAL_IP` → `REMOTE_ADDR` → `HTTP_X_FORWARDED_FOR`) — mascara o problema em vez de o resolver | Configurar explicitamente `howGetIPs`, não confiar no fallback silencioso |
| `wfpendingissues` vazia mesmo com `scansEnabled_checkHowGetIPs=1` e `howGetIPs` mal configurado | O scan periódico pode não ter corrido recentemente, ou o alerta já expirou/foi dispensado | Não usar `wfpendingissues` vazia como prova de "nada errado" — cruzar sempre com leitura directa da config |
| Activar `loginSec_requireAdminTwoFactor` sem aviso prévio | Bloqueia o fluxo normal de login até o admin configurar um autenticador | Confirmar com o utilizador antes; garantir acesso alternativo (SSH/SQL) para reverter |
| Prefixo de tabela assumido como `wp_` | Sites CWP podem ter prefixo custom (confirmado `wpne_` no piloto) | `wp db prefix --allow-root --path=$PATH` sempre primeiro |
| Editar 2FA/passkeys/CAPTCHA via `UPDATE ${PREFIX}wfconfig` sem efeito | Essas definições vivem em `wfls_settings` (colunas `name`/`value`, não `name`/`val`) — ver §6 | `UPDATE ${PREFIX}wfls_settings SET value='...' WHERE name='...';` |
| Editar regras/bloqueios do WAF directamente em `wp-content/wflogs/*.php` | São ficheiros PHP serializados com `__halt_compiler()`, regenerados a partir da BD — editar à mão desalinha-os da BD e pode partir o WAF | Alterar sempre via `wfconfig`/UI; a sincronização para ficheiro é automática (§10) |
| Achar que `loginSec_requireAdminTwoFactor=0` prova que 2FA nunca é obrigatório | Essa chave é legada; sites migrados usam `required-2fa-role.<role>` dentro de `wfls_settings` | Verificar sempre `required-2fa-role.*` e as capabilities `wf2fa_*` do role antes de concluir (§6) |
| Assumir que `maxRequestsHumans`/`maxRequestsCrawlers`="DISABLED" significa "sem rate limiting nenhum" | Cada um dos 5 limitadores (§9) é independente — `maxGlobalRequests` pode estar activo e a aplicar-se a toda a gente mesmo com os outros 4 desligados | Verificar sempre os 5 pares `<limite>`/`<limite>_action`, não só um |
---
*Fonte: `CONFIG-Plugins-Referencia.md` §8 (Wordfence) + verificação
SQL/SSH ao vivo em `emanuelalmeida.pt` (16-08-2026, WP 7.0.4/PHP 8.2.31,
Wordfence 9.0.0 gratuito) — schema de `wfconfig` (313 chaves confirmadas
via `SELECT DISTINCT name`), contagem real de 2FA/passkeys,
`detectProxyRecommendation`, valores de scan e login security. **Expandido
nesta sessão** com leitura completa do código-fonte do plugin (466
ficheiros PHP, ~147k linhas) — `lib/wfConfig.php` (schema completo de
defaults, 3 grupos: `checkboxes`/`otherParams`/`defaultsOnly`),
`lib/wfJavascriptBridge.php` (labels humanos de todas as opções da UI),
`models/block/wfBlock.php` e `wfRateLimit.php` (tipos de bloqueio e os 5
limitadores de rate limiting), `models/firewall/wfFirewall.php` (modos e
scoring do WAF), `lib/wfDiagnostic.php` (secções de diagnóstico),
`lib/wfImportExportController.php` (mecanismo real de import/export via
token wordfence.com), `lib/wfCentralAPI.php` (integração Central) e o
módulo `modules/login-security/` completo — incluindo a descoberta de que
2FA/passkeys/CAPTCHA vivem numa tabela SQL separada (`wfls_settings`,
colunas `name`/`value`) e que o WAF mantém uma cópia de config em
ficheiros PHP serializados (`wp-content/wflogs/config*.php`), fora de
`wfconfig`. Confirmado ao vivo: `wfls_settings` completa, capabilities
`wf2fa_*`/`wfls_*` do role administrator, ficheiros WAF (`config.php`,
`config-synced.php`, `rules.php`), categorias de regra (`sqli`/`xss`/`rce`)
e ligação activa a Wordfence Central desde 27/02/2025.*