Files
Claude Code 4a55d51329 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)
2026-08-19 03:44:00 +01:00

527 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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).