Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
68853ef
Changes
lovable-dev[bot] Apr 27, 2026
cd97caa
Reforçou gate de typecheck no CI
lovable-dev[bot] Apr 27, 2026
8067b0b
Changes
lovable-dev[bot] Apr 27, 2026
8dae703
Changes
lovable-dev[bot] Apr 27, 2026
e5d862f
Changes
lovable-dev[bot] Apr 27, 2026
3b02a0a
Changes
lovable-dev[bot] Apr 27, 2026
31f5919
Adicionou gate de npm audit CI
lovable-dev[bot] Apr 27, 2026
7bb5bd9
Changes
lovable-dev[bot] Apr 27, 2026
6c79bb8
Changes
lovable-dev[bot] Apr 27, 2026
905676f
Changes
lovable-dev[bot] Apr 27, 2026
035ba00
Changes
lovable-dev[bot] Apr 27, 2026
93efe74
Changes
lovable-dev[bot] Apr 27, 2026
335b325
Changes
lovable-dev[bot] Apr 27, 2026
6284f98
Implementou MFA enforcement
lovable-dev[bot] Apr 27, 2026
d795848
Changes
lovable-dev[bot] Apr 27, 2026
e9a0d95
Changes
lovable-dev[bot] Apr 27, 2026
d1b03e4
Changes
lovable-dev[bot] Apr 27, 2026
0b652a3
Changes
lovable-dev[bot] Apr 27, 2026
f07a4c5
Changes
lovable-dev[bot] Apr 27, 2026
a1e404c
Changes
lovable-dev[bot] Apr 27, 2026
a44f22b
Changes
lovable-dev[bot] Apr 27, 2026
1f9e1a1
Changes
lovable-dev[bot] Apr 27, 2026
97cf441
Changes
lovable-dev[bot] Apr 27, 2026
2f3c3c6
Changes
lovable-dev[bot] Apr 27, 2026
b59105f
Changes
lovable-dev[bot] Apr 27, 2026
bd23e56
Changes
lovable-dev[bot] Apr 27, 2026
21a24a6
Instrumentou rotas críticas
lovable-dev[bot] Apr 27, 2026
942c20f
Changes
lovable-dev[bot] Apr 27, 2026
6321605
Changes
lovable-dev[bot] Apr 27, 2026
548aa73
Changes
lovable-dev[bot] Apr 27, 2026
5217c37
Changes
lovable-dev[bot] Apr 27, 2026
23c78eb
Changes
lovable-dev[bot] Apr 27, 2026
69b53fb
Configurou alertas de webhooks
lovable-dev[bot] Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,14 @@ jobs:
- name: Run tests with coverage
run: npm run test:coverage

- name: Security audit
run: npm audit --audit-level=high || true
continue-on-error: true
# 🔐 SECURITY GATE — bloqueia merge quando aparecem vulnerabilidades
# NOVAS ≥ high (omit=dev) comparadas com .security/npm-audit-baseline.json.
# Para aceitar um CVE conhecido sem patch: revisar, justificar no PR e
# rodar `UPDATE_BASELINE=1 node scripts/check-npm-audit.mjs` localmente.
- name: 🔐 npm audit gate (no new vulnerabilities)
env:
AUDIT_THRESHOLD: high
run: node scripts/check-npm-audit.mjs

# Gate crítico: funções SECURITY DEFINER em public NÃO podem ser
# executáveis por PUBLIC/anon (fora da whitelist público-intencional)
Expand All @@ -122,6 +127,12 @@ jobs:
- name: 📜 Edge structured-logging gate
run: node scripts/check-edge-structured-logging.mjs

# Garante que TODA invocação de edge nas rotas críticas (auth/quote/mcp/
# magicUp/comparison/connections) use createClientLogger + log.headers()
# para correlação request_id ↔ edge logs. Allowlist congelada 2026-04-27.
- name: 📜 Client structured-logging gate (rotas críticas)
run: node scripts/check-client-structured-logging.mjs

- name: RLS policy tests (optional)
if: env.TEST_SELLER_PASSWORD != '' && env.TEST_ADMIN_PASSWORD != ''
env:
Expand Down Expand Up @@ -292,7 +303,12 @@ jobs:
if-no-files-found: ignore

edge-functions-typecheck:
name: Edge Functions — Deno typecheck
# 🔐 SECURITY GATE — falha o CI imediatamente quando qualquer edge function
# quebra o typecheck Deno após mudanças de segurança (CORS, authz, JWT,
# RLS bridge, secrets). Roda em paralelo ao `smoke` e NÃO depende de
# `quality` (sem `npm ci`) para dar feedback em <2min, antes da bateria
# pesada de testes. Ver scripts/typecheck-edge-functions.mjs.
name: 🔐 Edge Functions — Deno typecheck (security gate)
runs-on: ubuntu-latest
timeout-minutes: 15

Expand All @@ -310,7 +326,24 @@ jobs:

# Roda `deno check` em cada supabase/functions/<fn>/*.ts.
# Falha o job se qualquer função tiver erro de tipos
# (ex: cast inseguro de GenericStringError, SupabaseClient com schema errado).
- name: Typecheck all edge functions
# (ex: cast inseguro de GenericStringError, SupabaseClient com schema
# errado, corsHeaders fora de escopo, getClaims sem null-check).
# Após mudanças de segurança (CORS allowlist, authz manifest, JWT,
# _shared/*), este gate falha ANTES de qualquer deploy.
- name: Typecheck all edge functions (fail-fast on security regressions)
run: node scripts/typecheck-edge-functions.mjs

# Resumo amigável: quais funções falharam, para PRs de segurança.
- name: Summarize failures (PR comment-friendly)
if: failure()
run: |
echo "::error title=Edge typecheck failed::Uma ou mais edge functions quebraram o typecheck Deno após mudanças de segurança. Rode localmente: node scripts/typecheck-edge-functions.mjs"
echo "## ❌ Edge Functions typecheck falhou" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Mudanças de segurança (CORS, authz, JWT, RLS) introduziram regressão de tipos em pelo menos uma edge function." >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Reproduzir localmente:**" >> "$GITHUB_STEP_SUMMARY"
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
echo "node scripts/typecheck-edge-functions.mjs" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"

1 change: 1 addition & 0 deletions .security/npm-audit-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"$schema":"./baseline.schema.json","generatedAt":null,"threshold":"high","knownAdvisories":[],"notes":"Atualize com UPDATE_BASELINE=1 node scripts/check-npm-audit.mjs após revisar cada CVE."}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O baseline foi commitado com generatedAt: null e sem a estrutura gerada pelo próprio gate (pretty JSON + timestamp ISO). Como o script já tem UPDATE_BASELINE=1 para gerar o formato canônico, vale atualizar este arquivo usando o script para evitar confusão e garantir que o baseline refletirá exatamente o filtro aplicado (threshold/omit=dev).

Suggested change
{"$schema":"./baseline.schema.json","generatedAt":null,"threshold":"high","knownAdvisories":[],"notes":"Atualize com UPDATE_BASELINE=1 node scripts/check-npm-audit.mjs após revisar cada CVE."}
{
"$schema": "./baseline.schema.json",
"generatedAt": "2026-04-30T00:00:00.000Z",
"threshold": "high",
"knownAdvisories": [],
"notes": "Atualize com UPDATE_BASELINE=1 node scripts/check-npm-audit.mjs após revisar cada CVE."
}

Copilot uses AI. Check for mistakes.
114 changes: 114 additions & 0 deletions docs/observability/webhook-alerts-spec.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
-- ============================================================================
-- Webhook Alerts Spec — Detection Queries (read-only)
-- ----------------------------------------------------------------------------
-- SSOT das queries que a edge function `webhook-alerts-monitor` executa para
-- decidir se dispara um alerta no Sentry. Nenhuma destas queries faz INSERT/
-- UPDATE — todas leem `public.webhook_delivery_metrics` (schema definido em
-- supabase/migrations/20260427122230_*.sql).
--
-- Colunas usadas:
-- occurred_at timestamptz — quando a tentativa ocorreu
-- source text — ex: 'bitrix', 'product-webhook', 'stripe'
-- direction text — 'inbound' | 'outbound'
-- http_status int — código HTTP (NULL para erro de transporte)
-- success boolean — sucesso final (após retries)
-- request_id text — correlation id
--
-- Janelas e thresholds (perfil CONSERVADOR — confirmado pelo usuário):
-- * delivery_failure_total : >=3 falhas CONSECUTIVAS por (source,direction)
-- * spike_5xx : >=5 respostas 5xx em 5min OU >20% das requests
-- (mín 10 amostras na janela)
-- * spike_4xx : >40% de respostas 4xx em 5min (mín 10 amostras)
--
-- Cada query devolve UMA linha por (source,direction) que ATINGE o threshold.
-- A edge monitor mapeia cada linha para um evento Sentry com tags:
-- alert=<id>, source=<source>, direction=<direction>, severity=warning|error
-- ============================================================================

-- :window_minutes -> default 5
-- :min_samples -> default 10
-- :rate_5xx -> default 0.20
-- :rate_4xx -> default 0.40
-- :abs_5xx -> default 5
-- :consecutive -> default 3

-- ----------------------------------------------------------------------------
-- 1) delivery_failure_total
-- >= N falhas CONSECUTIVAS (mais recentes) por (source,direction).
-- ----------------------------------------------------------------------------
with recent as (
select
source,
direction,
success,
occurred_at,
row_number() over (partition by source, direction order by occurred_at desc) as rn
from public.webhook_delivery_metrics
where occurred_at > now() - (:window_minutes || ' minutes')::interval
)
select
source,
direction,
count(*) filter (where success = false) as failures,
max(occurred_at) as last_failure_at,
'delivery_failure_total' as alert_id,
'error' as severity
from recent
where rn <= :consecutive
group by source, direction
having count(*) filter (where success = false) >= :consecutive;

-- ----------------------------------------------------------------------------
-- 2) spike_5xx
-- >= :abs_5xx respostas 5xx OU >:rate_5xx das requests na janela.
-- ----------------------------------------------------------------------------
with bucket as (
select
source,
direction,
count(*) as total,
count(*) filter (where http_status between 500 and 599) as count_5xx
from public.webhook_delivery_metrics
where occurred_at > now() - (:window_minutes || ' minutes')::interval
group by source, direction
)
select
source,
direction,
total,
count_5xx,
round(100.0 * count_5xx / nullif(total,0), 2) as pct_5xx,
'spike_5xx' as alert_id,
'error' as severity
from bucket
where total >= :min_samples
and (
count_5xx >= :abs_5xx
or (count_5xx::float / nullif(total,0)) > :rate_5xx
);

-- ----------------------------------------------------------------------------
-- 3) spike_4xx
-- > :rate_4xx das requests com http_status 4xx.
-- ----------------------------------------------------------------------------
with bucket as (
select
source,
direction,
count(*) as total,
count(*) filter (where http_status between 400 and 499) as count_4xx
from public.webhook_delivery_metrics
where occurred_at > now() - (:window_minutes || ' minutes')::interval
group by source, direction
)
select
source,
direction,
total,
count_4xx,
round(100.0 * count_4xx / nullif(total,0), 2) as pct_4xx,
'spike_4xx' as alert_id,
'warning' as severity
from bucket
where total >= :min_samples
and (count_4xx::float / nullif(total,0)) > :rate_4xx;
71 changes: 71 additions & 0 deletions docs/observability/webhook-alerts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Webhook Alerts → Sentry

Monitor read-only de `public.webhook_delivery_metrics` que dispara alertas no
Sentry quando há falhas de delivery ou spikes de 4xx/5xx por `(source,
direction)`.

## Componentes

| Arquivo | Papel |
|---|---|
| `docs/observability/webhook-alerts-spec.sql` | SSOT das queries de detecção (parametrizadas, read-only). |
| `supabase/functions/webhook-alerts-monitor/index.ts` | Cron edge function: detecta + envia eventos ao Sentry via envelope API. |

## Thresholds (perfil conservador)

Janela: **5 minutos**. Mínimo de **10 amostras** para spikes (evita falso-positivo em baixo volume).

| Alert | Critério | Severity |
|---|---|---|
| `delivery_failure_total` | ≥3 falhas **consecutivas** por (source,direction) | `error` |
| `spike_5xx` | ≥5 respostas 5xx **OU** >20% das requests | `error` |
| `spike_4xx` | >40% das requests com 4xx | `warning` |

Para alterar thresholds, edite as constantes no topo de
`webhook-alerts-monitor/index.ts` (SSOT do runtime) e os defaults no
spec SQL.
Comment on lines +24 to +26
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aqui o doc diz que o spec SQL tem “defaults” e que o runtime é SSOT no TS; porém a função TS atualmente não executa o spec SQL (faz agregação em memória). Para evitar drift, ou alinhe o runtime para executar o spec, ou ajuste este parágrafo para não sugerir que alterar o spec SQL afetará o comportamento do monitor.

Suggested change
Para alterar thresholds, edite as constantes no topo de
`webhook-alerts-monitor/index.ts` (SSOT do runtime) e os defaults no
spec SQL.
Para alterar thresholds do monitor, edite as constantes no topo de
`webhook-alerts-monitor/index.ts` (SSOT do runtime). O spec SQL é apenas uma
referência documental no estado atual e deve ser mantido em sincronia
manualmente, sem afetar o comportamento do monitor.

Copilot uses AI. Check for mistakes.

## Tags Sentry (para filtragem/dashboards)

Cada evento sai com:
- `alert` = `delivery_failure_total | spike_5xx | spike_4xx`
- `source` = ex. `bitrix24`, `product-webhook`, `inbound:n8n`
- `direction` = `inbound | outbound`
- `severity` = `warning | error`

E `fingerprint = ["webhook-alert", alert, source, direction]` — o Sentry agrupa
o mesmo alerta na mesma issue, evitando flood.

## Configuração

1. **Secret**: cadastrar `SENTRY_DSN_SERVER` (DSN do projeto Sentry usado em
server-side). Sem o secret, o monitor roda em modo "dry" e apenas loga
`alert_skipped_no_sink`.
2. **Tabela**: `public.webhook_delivery_metrics` já existe (migration
`20260427122230_*`) — colunas usadas: `occurred_at`, `source`, `direction`,
`http_status`, `success`, `request_id`.
3. **Cron**: registrar via `supabase--insert` em `cron.schedule`:
```sql
select cron.schedule(
'webhook-alerts-monitor',
'*/1 * * * *',
$$
select net.http_post(
url := '<SUPABASE_URL>/functions/v1/webhook-alerts-monitor',
headers := jsonb_build_object(
'Content-Type','application/json',
'Authorization','Bearer <ANON_KEY>'
),
Comment on lines +47 to +58
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O exemplo de cron usa Authorization: Bearer <ANON_KEY>, mas ANON_KEY é público e não autentica o caller. Como a edge function usa service_role internamente (bypass RLS) e pode disparar alertas, isso deveria ser protegido com um secret dedicado (header tipo X-Cron-Secret) ou outra forma de autenticação não-pública; atualize o exemplo para refletir esse gate.

Copilot uses AI. Check for mistakes.
body := '{}'::jsonb
);
$$
);
```

## Próximos passos (fora deste escopo)

- Criar a migration de `webhook_delivery_metrics` + índices.
- Instrumentar `webhook-inbound`, `webhook-dispatcher`, `product-webhook` para
gravar uma linha por delivery.
- Adicionar Sentry Alert Rule no projeto: "create issue when tag `alert` matches
`spike_5xx|delivery_failure_total`" → notifica Slack/email.
125 changes: 125 additions & 0 deletions scripts/check-client-structured-logging.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env node
/**
* 📜 Client Structured-Logging Gate (rotas críticas)
* ----------------------------------------------------------------
* Garante que toda invocação de edge function (`supabase.functions.invoke(...)`)
* em arquivos das rotas críticas (auth, quote, mcp, magicUp, comparison,
* connections) use `createClientLogger` E propague `headers: log.headers()`
* para correlacionar request_id entre client e edge logs.
*
* Critério: arquivo monitorado contém `supabase.functions.invoke` →
* DEVE conter `createClientLogger` E `log.headers()` (ou equivalente
* via REQUEST_ID_HEADER) em pelo menos uma chamada.
*
* Allowlist: arquivos legados podem ser registrados em ALLOWLIST com data e
* ticket — não pode crescer (snapshot 2026-04-27).
*
* Saída:
* exit 0 → todos os arquivos monitorados conformes
* exit 1 → arquivos faltando logger estruturado
*/
import { readFileSync, statSync } from 'node:fs';
import { resolve, dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
Comment on lines +22 to +24
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imports relative e execSync não são usados neste script. Remover evita ruído e ajuda a manter o gate mínimo e fácil de auditar (especialmente por ser um script de CI).

Suggested change
import { resolve, dirname, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

Copilot uses AI. Check for mistakes.

const __filename = fileURLToPath(import.meta.url);
const ROOT = resolve(dirname(__filename), '..');

// Domínios críticos: glob patterns relativos a src/.
const MONITORED_GLOBS = [
'src/contexts/AuthContext.tsx',
'src/pages/Auth.tsx',
'src/hooks/useStepUpAuth.ts',
// quote
'src/hooks/useQuotes.ts',
'src/pages/public-approval/usePublicQuoteApproval.ts',
'src/pages/quote-view/QuoteActionHandlers.ts',
'src/pages/quote-view/QuoteBitrixSync.ts',
// mcp
'src/components/admin/security/keys/useMcpKeys.ts',
'src/components/admin/security/keys/UpdateMcpKeyDialog.tsx',
'src/components/admin/security/keys/diagnostics/FullOpDiagnosticsPanel.tsx',
// magic up
'src/hooks/useMagicUpGeneration.ts',
// comparison
'src/pages/PublicComparisonPage.tsx',
// connections
'src/hooks/useConnectionTester.ts',
'src/hooks/useSecretsManager.ts',
];

// Snapshot 2026-04-27 — arquivos legados pendentes de instrumentação.
// NÃO PODE CRESCER. Cada entrada deve ter ticket no roadmap.
const ALLOWLIST = new Set([
'src/pages/Auth.tsx',
'src/pages/quote-view/QuoteActionHandlers.ts',
'src/pages/quote-view/QuoteBitrixSync.ts',
'src/components/admin/security/keys/UpdateMcpKeyDialog.tsx',
'src/components/admin/security/keys/diagnostics/FullOpDiagnosticsPanel.tsx',
]);

const violations = [];
const missingFiles = [];

for (const rel of MONITORED_GLOBS) {
const abs = resolve(ROOT, rel);
let content;
try {
statSync(abs);
content = readFileSync(abs, 'utf-8');
} catch {
missingFiles.push(rel);
continue;
}

const invokesEdge = /supabase\.functions\.invoke\s*\(/.test(content);
if (!invokesEdge) continue; // arquivo não chama edge → fora de escopo

const usesLogger = /createClientLogger\s*\(/.test(content);
const propagatesHeaders =
/log\.headers\s*\(\s*\)/.test(content) ||
/REQUEST_ID_HEADER/.test(content);

if (usesLogger && propagatesHeaders) continue; // ✅

Comment on lines +76 to +85
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este gate afirma que “toda invocação” de supabase.functions.invoke(...) deve propagar headers: log.headers(), mas o critério implementado só verifica se o arquivo contém createClientLogger( e log.headers() em QUALQUER lugar. Um arquivo pode ter múltiplas invocações e ainda passar mesmo com algumas chamadas sem headers. Sugestão: iterar por ocorrências de supabase.functions.invoke( e validar por chamada (heurística por bloco/linha) ou pelo menos exigir que TODAS as ocorrências contenham headers: próximo da invocação.

Copilot uses AI. Check for mistakes.
if (ALLOWLIST.has(rel)) continue; // legado conhecido

violations.push({
file: rel,
missing: [
!usesLogger && 'createClientLogger',
!propagatesHeaders && 'log.headers() / REQUEST_ID_HEADER',
].filter(Boolean).join(' + '),
});
}

console.log(`📜 Client structured-logging gate — ${MONITORED_GLOBS.length} arquivo(s) monitorado(s)`);
console.log(` Allowlist legada: ${ALLOWLIST.size} | Não-encontrados: ${missingFiles.length}`);

if (missingFiles.length > 0) {
console.warn('\n⚠️ Arquivos monitorados ausentes (atualize MONITORED_GLOBS):');
for (const f of missingFiles) console.warn(` - ${f}`);
}

if (violations.length === 0) {
console.log('\n✅ Todas as rotas críticas instrumentadas com logger estruturado.');
process.exit(0);
}

console.error(`\n❌ ${violations.length} arquivo(s) sem logger estruturado:\n`);
for (const v of violations) {
console.error(` • ${v.file}`);
console.error(` Falta: ${v.missing}`);
}
console.error(`
Como corrigir:
import { createClientLogger } from "@/lib/telemetry/structuredLogger";
const log = createClientLogger("<scope>", { base: { ... } });
log.info("<event>_start", { ... });
await supabase.functions.invoke("<fn>", { body, headers: log.headers() });
log.info("<event>_ok"); // ou log.error("<event>_failed", { err })

Padrão de event names: <scope>_start | _ok | _failed | _denied | _invalid
`);
process.exit(1);
Loading