| name | cg-blocker-resolution |
| description | Use SEMPRE que o usuário mencionar critical gap, CG-001, CG-002, CG-003, fechamento de bloqueador, pentest blocker, Bradesco blocker, hardcoded policy hash, audit gate ausente, shred_key simulado, localStorage JWT, AES-128 → AES-256-GCM, ou qualquer hygiene item que trava demo bancária. Triggers literais: "CG-001", "CG-002", "CG-003", "critical gap", "bloqueador Bradesco", "pentest", "policy_snapshot_hash", "shred_key fake", "JWT localStorage", "hygiene". NÃO use para bugs comuns ou refactors genéricos — esta skill é cirúrgica para os 4 críticos abertos do REX Guard. |
CG Blocker Resolution — Fechamento Cirúrgico
Quatro itens travando Bradesco ($25K USD expansion approved, demo aprovada). Cada CG aberto é credibilidade perdida com Glauco Sampaio (CISO Brief promete coisas que código não entrega). Esta skill é o playbook de fechamento — sem fluff, sem "melhorias genéricas", critério de aceite explícito por CG.
CG-001 — policy_snapshot_hash hardcoded
Sintoma
Hash da policy ativa está fixo no código. Toda inferência sela com o mesmo hash, independente da policy real em vigor. Auditoria forense fica impossível: "qual policy estava ativa quando a decisão X foi tomada?" → resposta única, sempre a mesma. Fraude técnica disfarçada de feature.
Root cause provável
Stub de desenvolvimento que migrou para prod porque ninguém puxou o cabo do // TODO: replace with dynamic load.
Fix
const POLICY_SNAPSHOT_HASH = 'sha256:abc123...';
import { createHash } from 'node:crypto';
interface PolicyResolver {
getActivePolicy(tenantId: string, modelId: string): Promise<PolicySnapshot>;
computeSnapshotHash(snapshot: PolicySnapshot): string;
}
class FirestorePolicyResolver implements PolicyResolver {
private cache = new LRUCache<string, PolicySnapshot>({
max: 1000,
ttl: 60_000,
});
async getActivePolicy(tenantId: string, modelId: string): Promise<PolicySnapshot> {
const cacheKey = `${tenantId}:${modelId}`;
const cached = this.cache.get(cacheKey);
if (cached) return cached;
const doc = await firestore
.collection('policies')
.doc(tenantId)
.collection('active')
.doc(modelId)
.get();
if (!doc.exists) {
throw new PolicyNotFoundError(`No active policy for ${tenantId}/${modelId}`);
}
const snapshot = doc.data() as PolicySnapshot;
this.cache.set(cacheKey, snapshot);
return snapshot;
}
computeSnapshotHash(snapshot: PolicySnapshot): string {
const canonical = canonicalize(snapshot);
return 'sha256:' + createHash('sha256').update(canonical).digest('hex');
}
}
Critério de aceite
- Grep
grep -rn "policy_snapshot_hash.*=.*'sha256:" src/ retorna zero matches
- Teste de integração: 2 policies diferentes ativas em 2 tenants → 2 hashes diferentes nos respectivos SealedRecibos
- Cache invalida em < 60s após policy update (medido em teste)
- Falha em resolver policy = inferência rejeitada (não fallback silencioso)
CG-002 — CI/CD audit gate ausente
Sintoma
Pipeline .github/workflows/deploy.yml (ou Cloud Build equivalente) não tem step de validação de audit trail antes de promover. Deploy passa mesmo se recibos_sealed está com schema drift, signature inválida, ou Merkle chain quebrada na última janela.
Fix
name: REX Guard Deploy
on:
push:
branches: [main]
tags: ['v*']
jobs:
audit-validation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Auth GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.AUDIT_VALIDATOR_SA }}
- name: Validate last 1000 recibos
run: |
./scripts/validate-audit-trail.sh \
--project=${{ secrets.PROJECT_ID }} \
--window=last-1h \
--min-recibos=1 \
--max-failure-rate=0
# Exit code != 0 = block deploy
- name: Validate Merkle chain integrity
run: |
npm run audit:verify-chain -- --hours=24 --strict
-
Script validate-audit-trail.sh minimal:
#!/usr/bin/env bash
set -euo pipefail
PROJECT=$1
WINDOW=$2
MIN=$3
MAX_FAIL=$4
QUERY="
SELECT
COUNT(*) as total,
COUNTIF(signature IS NULL OR merkle_root IS NULL) as missing_proofs,
COUNTIF(LENGTH(policy_snapshot_hash) != 71) as malformed_hash
FROM \`${PROJECT}.audit_trail.recibos_sealed\`
WHERE sealed_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
"
result=$(bq query --use_legacy_sql=false --format=json "$QUERY")
total=$(echo "$result" | jq -r '.[0].total')
fails=$(echo "$result" | jq -r '.[0].missing_proofs + .[0].malformed_hash')
if [ "$total" -lt "$MIN" ]; then
echo "FAIL: only $total recibos in window (min $MIN)"
exit 1
fi
fail_rate=$(echo "scale=4; $fails / $total" | bc)
if (( $(echo "$fail_rate > $MAX_FAIL" | bc -l) )); then
echo "FAIL: failure rate $fail_rate > $MAX_FAIL"
exit 1
fi
echo "PASS: recibos, failures"
Critério de aceite
- PR com Merkle chain quebrada na window de teste = pipeline bloqueia
- PR com schema drift (campo extra ou faltando) = pipeline bloqueia
- Tempo de gate < 90s (não pode virar friction de produtividade)
- Logs do gate vão pro audit trail (meta-auditoria)
CG-003 — shred_key() simulado
Sintoma
Função shred_key() retorna true sem destruir nada. Zero-Persistence virou narrativa de marketing. Se Glauco pedir prova de crypto-shredding em pentest, FoundLab vai precisar explicar por que CISO Brief mente. Risco de credibilidade: total.
Root cause
Provavelmente foi marcado como "implementar depois" para destravar demo, e ninguém voltou. Padrão clássico de débito técnico que vira fraude.
Fix
Ver skill audit-trail-bcb538 seção "Crypto-shredding (CG-003 fix)" para implementação canônica. Resumo do critério de aceite:
Critério de aceite
- Teste E2E: usuário chama deleção LGPD →
shred_key() executa → Cloud KMS Console mostra version DESTROY_SCHEDULED
- Após 24h+ (grace period KMS), tentativa de descriptografar payload original retorna
KeyNotFoundError
- Audit event
CRYPTO_SHRED_INITIATED está em recibos_sealed ou tabela paralela lgpd_deletions
- SealedRecibo do payload original permanece íntegro e verificável (provando que apenas a chave morreu, não o registro)
- Documentação para auditor externo: como provar deleção criptográfica, quais artefatos KMS apresentar
Hygiene 1 — JWT no localStorage → httpOnly cookie
Sintoma
Frontend armazena JWT em localStorage. Vulnerável a XSS — qualquer script injetado lê o token. Padrão OWASP Top 10 violado em produto que vende segurança.
Fix
Backend:
reply
.setCookie('rex_session', jwt, {
httpOnly: true,
secure: true,
sameSite: 'strict',
domain: '.foundlab.com.br',
path: '/',
maxAge: 3600,
signed: true,
})
.send({ user: sanitizedUser });
Frontend:
fetch(API_URL, {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
CSRF protection adicional:
- Endpoint
/csrf-token que retorna token sincronizador
- Header
X-CSRF-Token validado em todas as mutations
- ou double-submit cookie pattern
Critério de aceite
grep -rn "localStorage" frontend/src/ retorna apenas ocorrências sem JWT/auth
- DevTools Application → Local Storage não mostra token
- Cookie
rex_session aparece com flags HttpOnly + Secure + SameSite=Strict
- Pentest XSS simulado não consegue exfiltrar credencial
Hygiene 2 — AES-128 → AES-256-GCM
Sintoma
Encryption em repouso (ou em trânsito interno) usa AES-128. Não é "inseguro" academicamente, mas para banco brasileiro auditado é olhar de lado. AES-256-GCM é o padrão de mercado e custa basicamente o mesmo em performance moderna.
Fix
const cipher = createCipheriv('aes-128-cbc', key, iv);
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
function encryptAESGCM(plaintext: Buffer, key: Buffer, aad?: Buffer): EncryptedPayload {
if (key.length !== 32) throw new Error('AES-256 requires 32-byte key');
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
if (aad) cipher.setAAD(aad);
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const authTag = cipher.getAuthTag();
return { iv, ciphertext, authTag, aad };
}
function decryptAESGCM(payload: EncryptedPayload, key: Buffer): Buffer {
decipher = (, key, payload.);
decipher.(payload.);
(payload.) decipher.(payload.);
.([decipher.(payload.), decipher.()]);
}
Migração de dados existentes:
- Re-encrypt em batch durante janela de manutenção
- Manter compatibilidade temporária (read-only AES-128 + write AES-256) por 1 ciclo
- Após validação, remover paths AES-128 completamente
Critério de aceite
grep -rn "aes-128" src/ retorna zero matches
- Todos os payloads novos usam AES-256-GCM com IV de 12 bytes
- Auth tag validado em decrypt (rejeitar payload tamperado)
- AAD usado quando contexto criptográfico precisa ser amarrado (ex: tenant_id como AAD)
Pentest readiness — checklist consolidado
Antes de liberar pentest pra Bradesco:
Boundaries (CRITICAL)
- NUNCA fechar CG sem teste automatizado que prove o fechamento
- NUNCA marcar CG como "closed" sem PR review por alguém que NÃO escreveu o fix
- NUNCA liberar pentest com qualquer CG aberto — risco de relatório público de vulnerabilidade crítica
- NUNCA alterar CISO Brief para "esconder" gap — se o Brief promete e código não entrega, o Brief muda OU o código entrega. Mentir é fraude.
- SEMPRE registrar fechamento de CG no audit trail com referência ao PR
- SEMPRE notificar Glauco Sampaio antes de marcar todos os CGs como fechados — ele é o validador externo, não decoração