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
@@ -0,0 +1,526 @@
---
name: seguranca-descomplicar
description: Gestão do plugin próprio "Segurança Descomplicar" (seguranca-descomplicar, v1.4.2) via WP-CLI e REST API nos 7 sites do bundle CWP. Cobre os 5 módulos — Security Headers (CSP/XFO/security.txt), Wordfence Agent (endpoint REST autenticado para sites sem SSH), Login/dash (URL de login customizada), Descomplicar Monitor (telemetria para dash.descomplicar.pt), Admin Hardening (Classic Editor, emails de update, nags) — com comandos `wp seguranca-descomplicar`, options `segdesc_*`, e gotchas reais de produção. Usar quando "seguranca-descomplicar", "plugin de segurança Descomplicar", "CSP", "X-Frame-Options", "security.txt", "wordfence agent", "login dash", "/dash", "descomplicar monitor", "admin hardening", "classic editor forçado", "esconder avisos de update", "token do agente", "allowlist IP wordfence agent".
---
# /seguranca-descomplicar — Plugin próprio de segurança WordPress (5 módulos)
Plugin normal (não mu-plugin, decisão explícita), instalado em
`wp-content/plugins/seguranca-descomplicar/`, **gerido inteiramente pela
Descomplicar®** — código-fonte único, versionado em
`71.Seguranca/plugin/seguranca-descomplicar/seguranca-descomplicar.php`,
distribuído por SCP/cp aos 7 sites do bundle. Substitui: `wordsense-agent.php`
(plugin antigo), mu-plugin `descomplicar-security-headers.php`, plugin
autónomo `descomplicar-monitor`, e todos os snippets WPCode/Code Snippets
de login `/dash` e admin hardening que estavam clonados byte-a-byte em 6 dos
7 sites.
**Fonte:** código-fonte completo lido nesta sessão (1494 linhas) +
`CONFIG-Plugins-Referencia.md` §2 (config ao vivo verificada nos 7 sites) +
`index.md` (changelog de 8 sessões de produção, bugs reais e correcções).
**Gerido/consumido por (ferramentas externas, fora desta skill):**
`71.03-SecHeaders/tools/secheaders.py` (módulo 1) e
`71.01-Wordsense/tools/wordsense.py`, transporte `http` (módulo 2).
---
## Contexto CWP — sempre obrigatório
```bash
# Local (quando o path existe no filesystem do agente)
wp seguranca-descomplicar <comando> --path=$PATH
# Via SSH (servidor real)
ssh -p 9443 root@server.descomplicar.pt \
"wp seguranca-descomplicar <comando> --path=$PATH --allow-root"
```
Namespace REST do plugin: `seguranca-descomplicar/v1` (todos os endpoints
exigem `Authorization: Bearer <token>`, token de 64 chars hex em
`segdesc_agent_token`).
---
## Decision tree — qual módulo/comando usar
| Preciso de | Módulo | Comando/opção |
|---|---|---|
| Ver/mudar CSP, XFO, security.txt | 1 | `wp seguranca-descomplicar headers status\|set` |
| Diagnosticar/ajustar Wordfence sem SSH (via HTTP) | 2 | REST `seguranca-descomplicar/v1/wordfence/*` |
| Ligar/mudar a URL de login custom | 3 | `wp seguranca-descomplicar login status\|enable\|disable\|slug\|block-wp-login` |
| Ver estado da telemetria para o dashboard | 4 | `wp option get descomplicar_monitor_*` (sem comando CLI próprio) |
| Ligar/desligar Classic Editor, emails de update, nags | 5 | `wp seguranca-descomplicar hardening status\|enable\|disable` |
| Rodar o token do agente REST | — | `wp seguranca-descomplicar token --rotate` |
| Restringir o REST endpoint a IPs específicos | — | `wp seguranca-descomplicar allowlist --set=IP1,IP2` |
---
## Referência de constantes (`SEGDESC_*`)
Todas as constantes definidas no plugin (topo do ficheiro, antes de
qualquer hook), agrupadas por função — confirma cobertura total do
código-fonte (26 constantes no total).
**Identificação**
| Constante | Valor | Nota |
|---|---|---|
| `SEGDESC_VERSION` | `'1.4.2'` | Devolvida em `wordfence/status.agent_version` |
| `SEGDESC_NS` | `'seguranca-descomplicar/v1'` | Namespace REST |
**Options (`segdesc_*`, persistidas em `wp_options`)** — ver tabelas de
cada módulo abaixo para defaults/comportamento; nomes reais:
`segdesc_agent_token`, `segdesc_headers_enabled`, `segdesc_csp`,
`segdesc_xfo`, `segdesc_csp_report_only`, `segdesc_security_txt`,
`segdesc_ip_allowlist`, `segdesc_nginx_csp_covered`,
`segdesc_login_enabled`, `segdesc_login_slug`,
`segdesc_login_block_wplogin`, `segdesc_hardening_enabled`,
`segdesc_hardening_classic_editor`, `segdesc_hardening_update_emails`,
`segdesc_hardening_update_nags`.
**Defaults/valores hardcoded (literais PHP usados como fallback, não são options)**
| Constante | Valor |
|---|---|
| `SEGDESC_DEFAULT_LOGIN_SLUG` | `'dash'` |
| `SEGDESC_DEFAULT_CSP` | CSP de enforcement (ver secção 1) |
| `SEGDESC_DEFAULT_XFO` | `'SAMEORIGIN'` |
| `SEGDESC_DEFAULT_CSP_REPORT_ONLY` | CSP Report-Only (ver secção 1) |
| `SEGDESC_DANGEROUS_KEYS` | `serialize(['waf_status', 'isPaid', 'apiKey'])` — chaves `wfconfig` que exigem `force_dangerous=true` em `/wordfence/config` |
**Rate-limit do endpoint REST** (só editável no código, sem option/wp-cli)
| Constante | Valor | Nota |
|---|---|---|
| `SEGDESC_RATE_LIMIT_WINDOW` | `60` (segundos) | Janela do tecto geral de pedidos |
| `SEGDESC_RATE_LIMIT_MAX` | `60` | Pedidos nominais/IP/janela (~30 reais, ver nota da dupla invocação) |
| `SEGDESC_RATE_LIMIT_FAIL_MAX` | `10` | Falhas nominais/IP/janela antes do lockout (~5 reais) |
| `SEGDESC_RATE_LIMIT_FAIL_LOCKOUT` | `900` (segundos = 15 min) | Duração do lockout por IP |
---
## 1. Security Headers — CSP, X-Frame-Options, security.txt
Hook `send_headers` (envia CSP/XFO/CSP-Report-Only) + hook `init` (serve
`/.well-known/security.txt`, RFC 9116).
### Options (`segdesc_*`)
| Option | Default | Nota |
|---|---|---|
| `segdesc_headers_enabled` | `'1'` (definida na activação) | Liga/desliga o módulo inteiro |
| `segdesc_csp` | `SEGDESC_DEFAULT_CSP` (definida na activação) | CSP de **enforcement** — só é enviada se `nginx_csp_covered` ≠ `'1'` |
| `segdesc_xfo` | `SAMEORIGIN` (definida na activação) | Sempre enviada, independente do nginx |
| `segdesc_csp_report_only` | `SEGDESC_DEFAULT_CSP_REPORT_ONLY` (fallback em código, não persistida na activação) | Sempre enviada — telemetria; único mecanismo de report em sites sem template nginx |
| `segdesc_security_txt` | `''` | Sem default — vazio = ficheiro não é servido; conteúdo bruto do `.well-known/security.txt` |
| `segdesc_ip_allowlist` | `''` (vazio = sem restrição) | CSV de IPs — só afecta os endpoints REST do módulo 2, não o site público |
| `segdesc_nginx_csp_covered` | `'0'` | **`'1'` em sites com o template nginx aplicado** — desliga o CSP de enforcement do plugin (evita intersecção de directivas entre 2 CSPs); XFO continua sempre activo |
### Comandos
```bash
# Estado
wp seguranca-descomplicar headers status --path=$PATH
# Actualizar CSP/XFO
wp seguranca-descomplicar headers set --csp="default-src 'self'; script-src 'self' https:" --path=$PATH
wp seguranca-descomplicar headers set --xfo=SAMEORIGIN --path=$PATH
# security.txt a partir de ficheiro local
wp seguranca-descomplicar headers set --security-txt-file=/tmp/security.txt --path=$PATH
# Ligar/desligar o módulo inteiro
wp seguranca-descomplicar headers set --enabled=1 --path=$PATH
# Opções que o wp-cli custom NÃO cobre — usar wp option directo
wp option get segdesc_nginx_csp_covered --path=$PATH
wp option update segdesc_nginx_csp_covered 1 --path=$PATH # sites com template nginx aplicado
wp option get segdesc_ip_allowlist --path=$PATH
```
### CSP real em produção (v1.4.2 — permissivo por directiva, sem lista branca de domínios)
```
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
style-src 'self' 'unsafe-inline' https:;
img-src 'self' data: https:;
font-src 'self' data: https:;
frame-src https:;
frame-ancestors 'self';
object-src 'none'
```
Decisão explícita do utilizador (10ª sessão): uma lista branca por domínio
(`frame-src youtube.com google.com ...`) é manutenção infinita — cada embed
novo obriga a voltar a editar. `frame-src https:` permite qualquer iframe
servido por HTTPS; o que interessa bloquear é `http://` não encriptado e
`data:`/`javascript:`. `object-src 'none'` e `frame-ancestors 'self'`
mantêm-se restritos (sem caso de uso legítimo / protecção anti-clickjacking
deste site).
**Nota de produção (16-08-2026):** na prática, no bundle CWP, CSP/XFO são
geridos maioritariamente pelo **template nginx** (`71.03-SecHeaders/`,
nível de servidor, sobrevive a cache/preload de plugin) nos sites que o
têm aplicado — o módulo do plugin fica então limitado a `security.txt` +
CSP Report-Only (telemetria, ver `segdesc_nginx_csp_covered`). Em
`emanuelalmeida.pt` (piloto desde a 11ª sessão), os headers passaram a
ser servidos por **Cloudflare Transform Rules** (token do plugin App for
Cloudflare®) — nginx e este plugin continuam a enviar os seus próprios
headers, mas ficam invisíveis ao tráfego real porque o `set` do
Cloudflare se sobrepõe antes de chegar ao browser; servem só de fallback
para acesso directo à origem (bypass do Cloudflare).
---
## 2. Wordfence Agent — endpoint REST autenticado (sites sem SSH)
8 rotas REST sob `seguranca-descomplicar/v1`, autenticação partilhada via
`segdesc_check_auth()` (allowlist → lockout → rate-limit → Bearer token).
Registadas sempre; `/wordfence/status` indica se o Wordfence está mesmo
activo no site.
| Rota | Método | Payload | Função |
|---|---|---|---|
| `/headers/status` | GET | — | Estado do módulo 1 |
| `/headers/set` | POST | `csp`, `xfo`, `security_txt`, `enabled` | Ajusta módulo 1 remotamente |
| `/wordfence/status` | GET | — | `wordfence_active`, `version`, `db_prefix` |
| `/wordfence/query` | POST | `sql` | **Só SELECT/SHOW contra exactamente 1 tabela `wf*`** — bloqueia JOIN, subquery, UNION, INSERT/UPDATE/DELETE/DROP/etc. |
| `/wordfence/config` | POST | `key`, `value`, `force_dangerous` | UPSERT em `{prefix}wfconfig`; chaves perigosas (`waf_status`, `isPaid`, `apiKey`) exigem `force_dangerous=true` |
| `/wordfence/unblock-ip` | POST | `ip` | `DELETE FROM {prefix}wfblockediplog WHERE IP = INET6_ATON(ip)` |
| `/wordfence/whitelist-ip` | POST | `ip`, `reason` | Acrescenta `"{ip} #{reason}"` a `neverBlockIP` em `wfconfig` |
| `/wordfence/scan` | POST | — | `wfScanEngine::startScan()` se a classe existir; senão `do_action('wordfence_doScan')` (fallback cron) |
**`/headers/*` — nota de proveniência:** os 2 endpoints REST de headers
(`status`/`set`) foram desenhados para o transporte `http` de
`71.03-SecHeaders/tools/secheaders.py` (comentário no código-fonte:
"usados por secheaders.py transporte 'http' (futuro, hoje só SSH)") —
presentes e funcionais, mas **ainda não consumidos em produção**:
`secheaders.py` aplica correcções via SSH/wp-cli directo, não via este
REST. Único consumidor real hoje é o próprio `wp seguranca-descomplicar
headers`.
### Autenticação e rate-limit (importante para clientes REST)
```
Authorization: Bearer <segdesc_agent_token>
```
- **Allowlist de IP** (opcional, `segdesc_ip_allowlist`) verificada primeiro.
- **Rate-limit** por IP: 60 pedidos/60s nominal — **mas o `WP_REST_Server`
invoca `permission_callback` DUAS VEZES por pedido HTTP real** (confirmado
ao vivo em `carstuff.pt`), pelo que o limite prático é ~30 pedidos reais,
não 60.
- **Lockout por falhas de auth:** 10 falhas nominais (~5 reais) → lockout de
15 min (900s), chaveado por IP via transient.
- **IP do cliente** (`segdesc_client_ip()`) só confia em `X-Forwarded-For`
quando `REMOTE_ADDR` é um proxy local confiável (`127.0.0.1`/`::1`) — as
portas backend do CWP (2083/2031/2095) são acessíveis directamente da
internet, por isso um pedido directo ao backend usa sempre `REMOTE_ADDR`,
nunca o XFF do atacante.
### Comandos WP-CLI relacionados
```bash
# Ver/rodar o token de autenticação do agente
wp seguranca-descomplicar token --path=$PATH
wp seguranca-descomplicar token --rotate --path=$PATH
# Allowlist de IP para os endpoints REST
wp seguranca-descomplicar allowlist --path=$PATH
wp seguranca-descomplicar allowlist --set=203.0.113.5,203.0.113.6 --path=$PATH
wp seguranca-descomplicar allowlist --clear --path=$PATH
```
### Exemplo de pedido real (curl)
```bash
TOKEN=$(wp seguranca-descomplicar token --path=$PATH)
curl -s https://site.pt/wp-json/seguranca-descomplicar/v1/wordfence/status \
-H "Authorization: Bearer $TOKEN"
```
---
## 3. Login /dash — URL de login customizada e segura
Substitui os antigos snippets WPCode "Alterar /wp-admin para /dash" (que
deixaram de executar por `E_DEPRECATED` em PHP 8.2 — ver Gotchas). Handler
em `template_redirect` prioridade 1 (antes do `redirect_canonical`,
prioridade 10).
### Comportamento quando `segdesc_login_enabled = '1'`
- `/<slug>` (default `dash`): mostra o form nativo de login WP se não
autenticado; `wp_redirect(admin_url())` se autenticado.
- `wp-login.php` directo (GET): **404** para não autenticados. O **POST**
do form (submissão real do login) continua a funcionar — o bloqueio só
intercepta GET.
- `/wp-admin/` directo: **404** para não autenticados, excepto
`admin-ajax.php` (necessário para plugins front-end).
### Hooks internos
| Hook | Prioridade | Função |
|---|---|---|
| `init` → `segdesc_login_rewrite_rule` | default (10) | Regista `add_rewrite_rule('^<slug>/?$', 'index.php?segdesc_login=1', 'top')` — só se o módulo estiver ligado |
| `query_vars` → `segdesc_login_query_var` | default (10) | Regista a query var `segdesc_login` (necessária para o WP reconhecer o parâmetro da rewrite rule) |
| `template_redirect` → `segdesc_login_handle` | **1** | Handler principal — antes do `redirect_canonical` (prioridade 10) |
| `init` → `segdesc_login_block_wp_login` | default (10) | Bloqueio de `wp-login.php` directo (GET) |
| `init` → `segdesc_login_block_wp_admin` | default (10) | Bloqueio de `/wp-admin/` directo (excepto `admin-ajax.php`) |
| `update_option_segdesc_login_slug` → `segdesc_login_flush_rules_on_slug_change` | default (10) | `flush_rewrite_rules()` automático sempre que `segdesc_login_slug` muda — **mesmo por `wp option update` directo**, não só pelo subcomando `login slug` |
### Options
| Option | Default | Nota |
|---|---|---|
| `segdesc_login_enabled` | `'0'` | Módulo desligado por omissão — só activo onde pedido |
| `segdesc_login_slug` | `'dash'` (constante `SEGDESC_DEFAULT_LOGIN_SLUG`, não persistida a menos que alterada) | Sanitizado para `[a-z0-9_-]` |
| `segdesc_login_block_wplogin` | `'1'` | Bloqueia `wp-login.php` directo (GET) |
### Comandos
```bash
wp seguranca-descomplicar login status --path=$PATH
wp seguranca-descomplicar login enable --path=$PATH # faz flush_rewrite_rules() automático
wp seguranca-descomplicar login disable --path=$PATH
wp seguranca-descomplicar login slug --set=acesso --path=$PATH
wp seguranca-descomplicar login block-wp-login --on --path=$PATH
wp seguranca-descomplicar login block-wp-login --off --path=$PATH
```
`status` devolve `enabled`, `slug`, `block_wp_login`, `login_url` (URL
completo já resolvido com `home_url()`).
**Nota carstuff.pt (achado 10ª sessão, fora do âmbito deste módulo):**
`/dash` naquele site serve login e `wp-login.php` redirecciona para
`/dash` por um mecanismo que **não é o Módulo 3** (nunca activado em
carstuff.pt até à migração completa) nem o snippet WPCode legado (sempre
em draft) — candidato mais provável é o `branda-white-labeling`
(login-screen custom). Login do admin funciona normalmente; não confundir
com este módulo ao diagnosticar `/dash` nesse site especificamente.
---
## 4. Descomplicar Monitor — telemetria para dash.descomplicar.pt
Classe `Descomplicar_Monitor` (singleton), **funde o antigo plugin autónomo
`descomplicar-monitor`** — reutiliza as MESMAS options
(`descomplicar_monitor_*`, prefixo diferente do resto do plugin, propositado
para preservar dados na migração) e o MESMO cron
(`descomplicar_monitor_cron`). Guard
`is_plugin_active('descomplicar-monitor/descomplicar-monitor.php')`: só
instancia se o plugin antigo não estiver activo (evita duplicar envios).
**Sem comando WP-CLI próprio** — este módulo não regista `WP_CLI::add_command`.
Interage-se via `wp option`, `wp eval`, ou a página de definições em
**Definições → Descomplicar Monitor** (`add_options_page`, capability
`manage_options`; grupo de settings `descomplicar_monitor_settings` via
`register_settings`, hook `admin_init`; AJAX `descomplicar_monitor_test` /
`descomplicar_monitor_send_now`, ambos com nonce próprio via
`check_ajax_referer`).
### Hooks internos
| Hook | Função |
|---|---|
| `admin_menu` → `add_admin_menu` | Regista a página **Definições → Descomplicar Monitor** |
| `admin_init` → `register_settings` | Regista o grupo `descomplicar_monitor_settings` (campos `api_key`, `enabled`) |
| `descomplicar_monitor_cron` → `send_monitoring_data` | Handler do cron — envia a telemetria |
| `cron_schedules` → `add_cron_interval` | Regista um intervalo **próprio** chamado `twice_daily` (43200s = 12h) — **não é o `twicedaily` nativo do WordPress** (nomes diferentes, mesmo valor); `wp_schedule_event` usa este nome custom |
| `wp_ajax_descomplicar_monitor_test` → `ajax_test_connection` | AJAX "Testar Conexão" |
| `wp_ajax_descomplicar_monitor_send_now` → `ajax_send_now` | AJAX "Enviar Agora" |
| `init` → `ensure_cron` | Reagenda `descomplicar_monitor_cron` se `wp_next_scheduled()` devolver vazio |
**Sem `register_deactivation_hook`** (nenhum módulo do plugin tem) — ao
desactivar o plugin, o evento `descomplicar_monitor_cron` continua
agendado no wp-cron, mas o código deixa de carregar, logo o disparo do
evento não faz nada (sem handler registado) — consome um ciclo de cron
sem efeito, sem erro visível. Reactivar retoma o envio automaticamente,
sem reconfigurar nada (options preservadas).
### Options
| Option | Nota |
|---|---|
| `descomplicar_monitor_enabled` | `'1'`/`'0'` — sem module flag `segdesc_*` (nome legado preservado) |
| `descomplicar_monitor_api_key` | Chave enviada como header `X-API-Key` |
| `descomplicar_monitor_last_sent` | Timestamp `Y-m-d H:i:s`, actualizado em cada tentativa (sucesso ou falha) |
| `descomplicar_monitor_last_status` | `'success'` ou a mensagem de erro |
### Dados recolhidos (POST JSON para `https://dash.descomplicar.pt/api/wp-monitor`)
URL, nome, **email de admin**, versão WP/PHP/MySQL, **flag multisite**,
**timestamp ISO 8601** (`current_time('c')`) e **timezone** do site, tema
(+ parent), lista de plugins activos c/ versão, updates pendentes
(core/plugins/themes, com bloco `counts` agregado), contagem de
posts/páginas/utilizadores/comentários, estatísticas da BD (**nº de
tabelas `{prefix}*`**, tamanho em MB, **prefix**, transients expirados,
tamanho do autoload em KB), memory_limit, upload máximo, debug mode, SSL,
cron disabled, cache habilitada, e um bloco `health` (status
good/warning/critical com lista de issues: `WP_DEBUG` sem log, sem SSL,
uploads sem permissão de escrita, memória PHP < 64MB).
**Nota:** `get_pending_updates()` chama `wp_update_plugins()` e
`wp_update_themes()` directamente antes de ler `get_plugin_updates()`/
`get_theme_updates()` — força uma verificação síncrona a
`api.wordpress.org` em **cada execução do cron de 12h**, mesmo que o
WordPress já tivesse um resultado em cache válido. Corre em contexto de
cron (não bloqueia carregamento de página), por isso não é o mesmo
problema de performance do Gotcha #3 (que era em `admin_init`/page load).
### Comandos úteis
```bash
# Estado
wp option get descomplicar_monitor_enabled --path=$PATH
wp option get descomplicar_monitor_last_status --path=$PATH
wp option get descomplicar_monitor_last_sent --path=$PATH
# Forçar envio imediato (fora do cron 12h)
wp eval 'echo json_encode(Descomplicar_Monitor::get_instance()->send_monitoring_data());' --path=$PATH
# Confirmar que o cron está agendado (reagenda-se sozinho em cada 'init' se limpo)
wp cron event list --path=$PATH --fields=hook,next_run_relative | grep descomplicar_monitor_cron
```
---
## 5. Admin Hardening — Classic Editor, emails de update, nags
Substitui 3 snippets WPCode/Code Snippets clonados byte-a-byte (`md5sum`
idêntico) em 6 dos 7 sites CWP (origem: tutorial WPBeginner). O snippet de
emails tinha um bug real herdado: `add_filter('auto_core_update_send_email',
'wpb_stop_auto_update_emails', ...)` referenciava uma função nunca definida
(`wpb_stop_update_emails`, sem `_auto`), gerando `E_WARNING` silencioso em
cada auto-update. O módulo simplifica para `__return_false` directo.
### Options
| Option | Default (quando `hardening_enabled='1'`) | Nota |
|---|---|---|
| `segdesc_hardening_enabled` | `'0'` | Módulo desligado por omissão |
| `segdesc_hardening_classic_editor` | `'1'` | `use_block_editor_for_post{,_type}` → `__return_false` |
| `segdesc_hardening_update_emails` | `'1'` | `auto_{core,plugin,theme}_update_send_email` → `__return_false` |
| `segdesc_hardening_update_nags` | `'1'` | Wipe de `admin_notices`/`all_admin_notices`/`network_admin_notices` (incl. avisos Wordfence) + CSS para esconder badges de contagem |
### Comandos
```bash
wp seguranca-descomplicar hardening status --path=$PATH
wp seguranca-descomplicar hardening enable --path=$PATH
wp seguranca-descomplicar hardening enable --no-classic-editor --path=$PATH # liga o módulo mas deixa o bloco editor
wp seguranca-descomplicar hardening enable --no-update-emails --path=$PATH
wp seguranca-descomplicar hardening enable --no-update-nags --path=$PATH
wp seguranca-descomplicar hardening disable --path=$PATH
```
### Como o bloqueio de nags funciona de facto (decisão de negócio: agressivo)
Decisão do Emanuel: bloquear TAMBÉM avisos Wordfence e visibilidade de
updates pendentes — clientes reencaminhavam esses avisos/emails achando que
eram falha da Descomplicar. Dois mecanismos distintos, **nunca tocando nos
transients de update** (ver Gotchas #3):
1. **`in_admin_header` prioridade 999** → `unset($wp_filter['admin_notices'])`
(+ `all_admin_notices` + `network_admin_notices`). Tem de ser
`in_admin_header`, não `admin_init` (ver Gotchas #2).
2. **`admin_head`** → injecta `<style>` que esconde
`#adminmenu .update-plugins`, `#wpadminbar .update-plugins`,
`#adminmenu .wf-menu-badge` (o badge "Wordfence N" no menu não é um hook
— é HTML injectado directamente no título do item de menu por
`wordfenceClass.php`; só CSS apanha).
---
## Deploy aos 7 sites do bundle
Fonte do plugin canónico:
`71.Seguranca/plugin/seguranca-descomplicar/` → copiado para
`wp-content/plugins/seguranca-descomplicar/` em cada site
(`tools/distribuir_e_activar_plugin.py`: `cp -r` local se o `path` existir
no filesystem, senão `mkdir -p` + `scp -P 9443` + `ssh ... wp plugin
activate` via `root@server.descomplicar.pt`).
| Domínio | Conta CWP | Path |
|---|---|---|
| `descomplicar.pt` | `ealmeida` | `/home/ealmeida/public_html` |
| `emanuelalmeida.pt` | `ealmeida` | `/home/ealmeida/emanuelalmeida.pt` |
| `carstuff.pt` | `carstuff` | `/home/carstuff/public_html` |
| `familyclinic.pt` | `familycl` | `/home/familycl/public_html` |
| `ignitionvortex.pt` | `ignition` | `/home/ignition/public_html` |
| `solarfvengenharia.com` | `solarfv` | `/home/solarfv/public_html` |
| `watercontrol.pt` | `wtc` | `/home/wtc/public_html` |
```bash
python3 tools/distribuir_e_activar_plugin.py
```
`tools/executar_pipeline_completa.py` orquestra o ciclo completo de
segurança nos mesmos 7 domínios (`SITES_CONFIG`, campo `tipo:
"nossos_sites"`): 1) diagnóstico externo (`seguranca-check.py`) → 2)
diagnóstico de postura interno (`postura.py` → wordsense + softcheck) → 3)
correcção automática de headers (`secheaders.py`, template nginx ou este
plugin) → 4) re-análise de verificação → 5) relatório visual. Usa
`--confirm-fix` para aplicar correcções (sem a flag, é dry-run/diagnóstico).
**Estado ao vivo confirmado (16-08-2026):** 5 módulos activos e coerentes
nos 7 sites — `headers_enabled=1`, `login_enabled=1` (7/7, antes eram 5/7),
`hardening_enabled=1` (7/7), `bridge`/monitor migrado 7/7. Único desvio real
por site: `segdesc_nginx_csp_covered` só é `'1'` nos sites com o template
nginx aplicado (todos excepto casos pontuais em rollout).
**Scripts sem flags de plugin:** `distribuir_e_activar_plugin.py` não
aceita argumentos CLI — itera sempre a lista `SITES` completa hardcoded
(`cp -r` local se o `path` existir, senão `mkdir -p` + `scp -P 9443` +
`ssh ... wp plugin activate` via `root@server.descomplicar.pt`, porta
9443). `executar_pipeline_completa.py` só tem uma flag, `--confirm-fix`
(sem ela = dry-run/diagnóstico só); a lista `SITES_CONFIG` mapeia cada
domínio para `wordsense_site`/`softcheck_account` — usados por
`postura.py`/`wordsense.py` (fora desta skill), não por este plugin
directamente.
---
## Gotchas / erros reais corrigidos em produção
| # | Sintoma | Causa raiz | Correcção |
|---|---|---|---|
| 1 | Actualizar `post_content` de um snippet WPCode via `$wpdb->update` directo não desliga o snippet legado | O WPCode mantém uma cache compilada em `wp_options.wpcode_snippets`, separada de `wp_posts` — não invalida por escrita SQL directa | Apagar a option `wpcode_snippets` ou chamar `wpcode()->cache->cache_all_loaded_snippets()` depois de qualquer edição directa em BD |
| 2 | Aviso "ElementsKit Lite Premium" (ou outro plugin) sobrevive ao wipe de `admin_notices` | Alguns plugins registam o callback em `admin_notices` **de dentro** de outro callback pendurado em `admin_head` — que corre **depois** de `admin_init` na sequência de bootstrap (`admin_init → load-{page} → admin_head → in_admin_header → admin_notices`) | Wipe tem de estar em `in_admin_header` prioridade 999 (o último ponto seguro antes de `admin_notices` disparar), nunca em `admin_init` |
| 3 | Regressão de performance real: ~1,3s extra em **todo** o carregamento do wp-admin (medido: core 0,4s + plugins 0,5s + temas 0,3s) | Bloquear `pre_site_transient_update_*` para esconder nags impede a cache nativa de 12h do WordPress de "pegar" — `_maybe_update_core/plugins/themes()` vêem sempre "nunca verificado" e disparam pedido HTTP síncrono a `api.wordpress.org` em cada page load | Nunca tocar nos transients de update; esconder badges/contadores só por CSS (`.update-plugins`) |
| 4 | Badge "Wordfence N" no menu lateral continua visível apesar do wipe de `admin_notices` | Não vem de nenhum hook — é HTML injectado directamente no título do item de menu por `wordfenceClass.php` | Só CSS resolve: `#adminmenu .wf-menu-badge { display: none !important; }` |
| 5 | `/dash` dá 404, `wp-login.php` também dá 404 — login completamente inacessível | Snippet WPCode legado usava `basename(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))` sem protecção — em PHP 8.2 `parse_url` pode devolver `null`, `basename(null)` dispara `E_DEPRECATED`; o WPCode apanha o erro e marca o snippet em `wpcode_snippets_errors`, **deixando de o executar por completo** (rewrite rule, query var e redirect todos mortos) | Migrado para código de plugin normal (Módulo 3), sem essa fragilidade; `wp rewrite flush --hard` depois de qualquer mudança de slug |
| 6 | Regra de rewrite `/<slug>` não persistia em alguns sites recém-activados | Rewrite rules não flushed após `login enable` em certas condições de cache de permalinks | `wp rewrite flush --hard` explícito após activar o módulo 3 |
| 7 | `security.txt` continha `mailto:seguranca@descomplicar.pt` — mailbox **inexistente** | Valor hardcoded em `tools/executar_pipeline_completa.py` (`--contact seguranca@descomplicar.pt`), replicado para os 7 sites no deploy inicial. Não foi apanhado pela auditoria automática (só validava presença/formato do ficheiro, não se o mailbox era real) | Corrigido nos 7 sites (`wp_options` ao vivo) e na origem (script) para `mailto:it@descomplicar.pt` — mesmo endereço usado em `alertEmails` do Wordfence, `rua`/`ruf` do DMARC, e utilizador `only-me` do WSAL |
| 8 | reCAPTCHA/YouTube bloqueados por CSP mesmo com `frame-src` a permitir | CSP duplicado (nginx + plugin) — browsers aplicam a directiva mais restritiva de **cada** CSP na intersecção; o nginx não declarava `frame-src`, caindo no fallback mais restrito | `segdesc_nginx_csp_covered = '1'` nos sites com template nginx — nginx passa a ser a única autoridade de enforcement, plugin só mantém XFO + Report-Only |
| 9 | `WP_REST_Server` conta o dobro dos pedidos reais nos limiares de rate-limit | `permission_callback` é invocado **duas vezes** por pedido HTTP real (confirmado por transient antes/depois de 1 pedido) | Limiares nominais (`FAIL_MAX=10`, `MAX=60`) valem, na prática, metade (~5 e ~30) — mantido assim de propósito, mais conservador não prejudica uso legítimo |
| 10 | `X-Forwarded-For` falsificável permitia contornar allowlist/rate-limit/lockout do endpoint REST | Portas backend do CWP (2083/2031/2095) acessíveis directamente da internet — confiar incondicionalmente em XFF permitia a um atacante que batesse directo no backend falsificar o IP visto | `segdesc_client_ip()` só confia em XFF quando `REMOTE_ADDR` é um proxy local confiável (`127.0.0.1`/`::1`), e só no último valor da cadeia (o que o próprio nginx anexa) |
| 11 | Desactivar o plugin não limpa cron/options/rewrite rules | Nenhum módulo regista `register_deactivation_hook` — só existe `register_activation_hook` (gera token + activa headers com CSP/XFO default) | `descomplicar_monitor_cron` fica agendado mas sem handler (evento inofensivo, sem efeito); opções `segdesc_*`/`descomplicar_monitor_*` e a rewrite rule do slug de login persistem; reactivar retoma o estado exactamente onde ficou — desejável para reinstalação, mas exige limpeza manual (`wp option delete`) numa desinstalação definitiva |
---
## Fonte
- `71.Seguranca/plugin/seguranca-descomplicar/seguranca-descomplicar.php`
(v1.4.2, 1494 linhas) — código-fonte completo, relido linha a linha
nesta expansão: todas as 26 constantes `SEGDESC_*`, os 6 hooks do
Módulo 3, os 7 hooks do Módulo 4 (incl. `cron_schedules` custom), os
8 endpoints REST (não 7 — corrigido nesta expansão), e os 5 subcomandos
WP-CLI (`token`, `allowlist`, `headers`, `login`, `hardening`)
confirmados exaustivos (sem `WP_CLI::add_command` adicional no
ficheiro).
- `71.Seguranca/CONFIG-Plugins-Referencia.md` §2 (`seguranca-descomplicar`)
— config ao vivo verificada via wp-cli nos 7 sites, 16-08-2026.
- `71.Seguranca/tools/distribuir_e_activar_plugin.py` e
`tools/executar_pipeline_completa.py` — relidos por completo; confirmado
que nenhum dos dois expõe flags de configuração do plugin em si (só
`--confirm-fix` no pipeline, sem argumentos no script de distribuição).
- `71.Seguranca/index.md` — lido na íntegra (linhas 1-627, não só
183-636 como na versão anterior desta skill); confirmou a contagem real
de 8 rotas REST (tabela de bundle da 12ª sessão: "8 rotas REST
(`/wordfence/*` × 6, `/headers/*` × 2)"), a nota de que CSP/XFO ficam
hoje maioritariamente a cargo do nginx/Cloudflare (módulo do plugin na
prática só serve `security.txt` nos sites com essa cobertura), e o caso
carstuff.pt (mecanismo de `/dash` alheio a este plugin).