BeryX — Segurança e conformidade
Documento público. É a resposta-padrão de segurança para RFPs e a fonte da secção «Segurança e conformidade» da página
/datavault/rfpdo site. Cada secção diz o que garantimos, onde está no código e que teste o prova. Não afirmamos nada que não esteja aqui.
Acesso read-only mecânico
O BeryX lê metadados, agregados e amostras limitadas — nunca escreve, move nem altera dados nas fontes do cliente. Não é só convenção nem só permissões pedidas ao cliente: a garantia é imposta no código.
- Conectores HTTP (Jira, Confluence, Databricks, SharePoint, Google Drive, …) só saem para a
rede pelo
ReadOnlyHttpClient(app/infra/connectors/http_guard.py): GET/HEAD/OPTIONS passam; POST/PUT/PATCH/DELETE são recusados antes de tocar na rede. - Nenhum conector pode expor um método de escrita: o decorator
enforce_read_onlyrecusa na definição da classe qualquer método com nome de escrita, e um teste percorre todos os conectores registados. - PostgreSQL: as sessões de profiling abrem com
default_transaction_read_only=onestatement_timeout— uma escrita, mesmo escondida numa função ou view, falha no servidor. - Snowflake (varredura de engenharia):
assert_catalog_statementrecusa tudo o que não seja um únicoSHOW/SELECT/DESCRIBEantes de chegar ao driver. - Recomendamos sempre um utilizador de serviço com permissões só de leitura — os passos por fonte estão no guia de conectores.
Testes: backend/tests/test_http_guard.py, backend/tests/test_connectors_registry.py.
Isolamento entre clientes (multi-tenant)
Cada cliente é um workspace, e o workspace é a fronteira dos dados. Um pedido autenticado resolve
sempre o workspace da sessão ou do token de API (get_current_workspace, app/api/deps.py), e
cada rota só lê e altera recursos desse workspace. A equipa da BeryxTech só lê outro workspace com um
acesso de suporte que um administrador desse cliente aprovou. Esse acesso tem prazo, é só de
leitura e fica no registo de auditoria.
Isolamento provado por teste sobre todas as rotas. A garantia não depende de alguém se lembrar de testar cada endpoint novo. O teste percorre as rotas registadas da API, com dois clientes de teste, A e B, semeados com um recurso de cada espécie:
- Entre clientes. Cada rota com um id no caminho recebe os ids de A e é chamada pelo administrador de B, o papel mais permissivo, e pelo token de API de B. Nunca pode responder 2xx nem 5xx. A recusa tem de ser igual à de um id que não existe, para que B nem saiba se o id de A existe. As rotas com o id no corpo ou na query passam pelo mesmo teste. Nas listagens, agregados e exports, nenhuma resposta a B pode conter um id ou email de A.
- Controlo positivo. O mesmo pedido feito por A chega aos seus recursos. Sem isto, o 404 de B não provava nada. O teste imprime a cobertura em cada execução do CI e falha abaixo de 90 % das rotas com id de cliente.
- Sem sessão. Todas as rotas respondem 401, exceto uma lista explícita e comentada de rotas públicas: saúde, login, SSO, webhooks assinados e links públicos por token. Essa lista tem de ser igual à tabela de decisão de acesso.
- Papéis. O perfil só de leitura recebe 403 em todas as alterações. Ficam de fora só o self-service da conta, o pedido de ajuda, o assistente, o feedback e a decisão de confiança em lote, que é uma leitura. O token de API só escreve nas operações previstas.
- Rotas novas. Uma rota nova com um parâmetro que o teste não sabe preencher faz o teste falhar. Só volta a passar quando a rota entra na matriz ou numa lista justificada.
- Rotas entre workspaces por desenho. A consola da equipa da BeryxTech, a organização e a pesquisa federada são provadas à parte, cada uma na sua fronteira. Por exemplo, um acesso de suporte sem aprovação recebe 403 em todas as leituras.
- O teste também morde. Um router de exemplo sem filtro de workspace é apanhado pelo mesmo varrimento.
Testes: backend/tests/test_tenant_isolation_matrix.py. As permissões por domínio dentro de um
workspace estão em backend/tests/test_access_matrix.py.
Amostras: o que sai da fonte e o que fica
O que um scan lê das tuas fontes, onde é processado e o que fica guardado:
- Perfil nas fontes SQL: agregados. Nas bases e warehouses SQL, o perfil (contagens, nulos,
distintos, mínimos e máximos) é calculado por SQL na própria fonte. Só voltam os números e uma
amostra de até 1 000 linhas das colunas perfiladas (
VALUE_SAMPLE_ROWS,app/infra/connectors/sql.py), lida em memória no worker para tirar até 20 valores de exemplo por coluna, que alimentam os tipos semânticos e a deteção de PII; só esses valores ficam gravados. Até à versão anterior eram 20 valores por coluna, numa query por coluna. Nas fontes sem SQL (ficheiros, APIs de SaaS, streaming), o conector lê os registos pela API oficial e o perfil é calculado no worker; o guia de conectores diz o que lê cada uma. - Regras de linha e duplicados: uma amostra de até 5 000 linhas por tabela. Em cada scan, as
regras que precisam de linhas e a deteção de duplicados leem no máximo 5 000 linhas por tabela
(
ROW_FETCH_LIMIT,app/domain/scans/service.py), escolhidas pela amostragem da própria fonte (TABLESAMPLE/SAMPLE), pelo hash da chave primária ou, sem nenhum dos dois, pelas primeiras linhas (método). É um teto, que vale também para a amostra do perfil: o DBA pode baixá-lo por fonte, até um mínimo de 100 linhas (max_sample_rows, «Boa vizinhança com o DBA»). As linhas são processadas em memória no worker, na UE (Google Cloud europe-west1); dessa leitura só ficam gravados os exemplos limitados descritos abaixo. - Leituras maiores só quando as pedes. A qualidade de rótulos (nos datasets em que a configuras), o data-diff e a correspondência entre datasets leem até 50 000 linhas por tabela, também em memória no worker.
- O que fica guardado, na UE: perfis, agregados, resultados de regras e scores, e exemplos limitados — por exemplo, até 20 valores por coluna no perfil, até 5 linhas por grupo de duplicados e até 5 valores truncados por regra violada. A tabela nunca é copiada para o BeryX.
- Com IA ativa, os exemplos enviados ao modelo seguem a política de IA do workspace. Por omissão vão à Anthropic, que os processa nos EUA com cláusulas contratuais-tipo. Para ficarem na UE, desliga a IA ou usa o Claude na tua conta cloud, no Bedrock ou no Vertex (plano Enterprise) — ver «Os teus modelos» e «Anonimização antes do LLM», abaixo.
- Execução no teu Databricks (ADR 0005, degrau B): ainda não corre nos scans. O pacote
beryx-profiler, que lê a tabela completa na tua conta e devolve só métricas, e o cliente da Jobs API já existem no código, mas nenhum scan o submete ainda. Até lá, também no Databricks as regras de linha e os duplicados usam a amostra acima. - Não temos hoje um modo em que nenhuma linha saia da tua rede (on-prem ou air-gapped). Um runner no teu perímetro (degrau C do mesmo ADR) depende de procura real.
Testes: backend/tests/test_seguranca_amostras.py (os números e o estado do Databricks acima são
comparados com o código).
Enriquecimento proposto: o que sai para cada fornecedor
O enriquecimento proposto pergunta a serviços públicos de referência, ou a uma instância Nominatim do próprio cliente, o valor que falta numa coluna. Cada fornecedor recebe só o campo a enriquecer, um valor de cada vez, e nunca a amostra completa nem as outras colunas da linha:
| Fornecedor (chave) | Host | O que recebe | O que devolve |
|---|---|---|---|
GeoAPI.pt (geoapi_pt) |
json.geoapi.pt |
O código postal CP7 | Localidade, concelho e distrito |
VIES, da Comissão Europeia (vies) |
ec.europa.eu |
Só o NIF de pessoa coletiva (1.º dígito 5, 6 ou 9). O NIF de uma pessoa singular não sai | Nome e morada registados |
Nominatim público, da OpenStreetMap Foundation (nominatim) |
nominatim.openstreetmap.org |
Só o código postal espanhol | Localidade e comunidade autónoma |
Instância Nominatim do cliente (nominatim_self_hosted) |
O do URL que o administrador indica | A morada, na geocodificação. É o único fornecedor que recebe dados pessoais | Latitude e longitude |
Os países e as moedas comparam-se com as listas ISO guardadas no BeryX, e nada sai.
- Opt-in por workspace e fail-closed. Por omissão, nenhum fornecedor recebe nada. Um
administrador autoriza cada um em «Regras → Fornecedores de enriquecimento»
(
PUT /api/v1/workspace/enrichment), e a autorização guarda quem consentiu e quando. Sem autorização, a proposta é recusada antes de ler a amostra da fonte. Esta autorização é separada da da regraaccuracy: autorizar o VIES numa não o autoriza na outra. - Moradas só para a instância do cliente. A geocodificação usa só a instância Nominatim do próprio cliente. A política de uso do Nominatim público pede que não se lhe enviem dados pessoais. Por isso o Nominatim público recebe só o código postal espanhol e autoriza-se sem a confirmação de dados pessoais. A mesma política limita o uso a 1 pedido por segundo. A OpenStreetMap Foundation guarda os dados dos pedidos no Reino Unido e nos Países Baixos (política de privacidade da OSMF).
- Um consentimento por destino. Para autorizar a instância, o administrador indica o URL
(
nominatim_url) e confirma que as moradas podem ser dados pessoais (confirm_personal_data). Mudar o URL é um destino novo e pede outra vez a confirmação, que regista quem a deu e quando. Retirar a autorização apaga o URL. Sem instância configurada e autorizada, nenhuma morada sai. - O URL da instância valida-se ao gravar. Tem de ser https (http só para hosts em
DATAVAULT_CONNECTOR_ALLOWED_HOSTS), sem utilizador nem palavra-passe, sem query nem fragmento e com até 500 caracteres. Os hosts doopenstreetmap.orgsão recusados, porque são o serviço público. A guarda anti-SSRF (SsrfGuard, embackend/app/infra/connectors/http_guard.py) recusa os hosts que resolvem para redes internas (loopback, redes privadas, metadata da cloud), salvo se o operador os autorizar emDATAVAULT_CONNECTOR_ALLOWED_HOSTS. O mesmo código normaliza o URL ao gravar e ao consultar (backend/app/infra/enrichment/providers.py), e cada consulta passa outra vez pela guarda, com o pinning de DNS. - O pedido à instância. O BeryX chama
<url>/searchcomq(a morada),format=jsonv2elimit=1, só por GET e com um User-Agent que o identifica. O pedido não leva credencial nem chave: uma instância atrás de autenticação não está suportada. - Fora da lista de subprocessadores. Os três serviços públicos não estão na lista pública de subprocessadores: só recebem valores de um workspace depois de um administrador desse workspace os autorizar, e cada um trata os pedidos segundo os seus próprios termos. A instância Nominatim é do próprio cliente, que escolhe o destino.
- Auditoria. Cada mudança nos fornecedores autorizados fica no registo de auditoria
(
enrichment_providers_updated), com a lista de antes e de depois, se houve confirmação de dados pessoais e, quando muda, o host da instância Nominatim. - Como sai. Só pedidos GET, pelo cliente HTTP só-leitura dos conectores, com a guarda anti-SSRF e o pinning de DNS. Os serviços públicos têm um intervalo mínimo entre pedidos em cada processo da API (0,5 s no VIES e 1 s na GeoAPI.pt e no Nominatim público) e cada proposta tem um teto de consultas. As respostas ficam numa cache em memória, separada por workspace e com a chave em hash (SHA-256). Os valores nunca vão para os logs.
- O que fica guardado. A sugestão guarda os valores propostos com a proveniência (fonte, data da consulta e confiança). Na geocodificação, a sugestão guarda só a proveniência e a cobertura: nem a morada nem as coordenadas, que a localizariam. As coordenadas propostas só aparecem na resposta ao pedido. O SQL da sugestão leva os códigos postais e os NIF de empresas que a fonte confirmou, com o valor de referência (no VIES, o nome e a morada registados da empresa). Na geocodificação, o SQL não leva nenhuma morada.
- O que a fonte não confirmou não fica. Um NIF de pessoa singular, um código postal que a fonte não conhece ou um texto livre na coluna não ficam na sugestão. Na resposta ao pedido, aparecem mascarados, só com os 3 últimos caracteres. O pull request leva a cobertura e a fonte, não a lista de valores.
O BeryX não escreve o valor proposto na fonte: o cliente aplica-o por pull request ou SQL, depois de aprovar a sugestão (o que não fazemos).
Testes: backend/tests/test_enrichment.py.
Credenciais cifradas e rotação de chaves
- As credenciais das fontes e os tokens de integração são cifrados com Fernet
(
app/domain/catalog/crypto.py) antes de serem gravados. Nunca são devolvidos pela API nem escritos em logs; as mensagens de erro dos vendors passam por um filtro que retira segredos. - Rotação de chaves:
DATAVAULT_ENCRYPTION_KEYSaceita várias chaves (MultiFernet): a primeira cifra, todas decifram.backend/scripts/rotate_fernet.pyre-cifra todos os segredos com a chave nova em lotes idempotentes; tokens que nenhuma chave abre são reportados só peloid, sem nunca serem decifrados para logs (app/domain/catalog/key_rotation.py).
Testes: backend/tests/test_fernet_rotation.py.
Proteção anti-SSRF com pinning de DNS
Os URLs das fontes vêm de configuração do cliente. Para que não possam apontar para dentro da
nossa infraestrutura (metadados da cloud, localhost, redes privadas):
SsrfGuard(app/infra/connectors/http_guard.py) só aceitahttpse recusa qualquer host que resolva para um endereço interno. É fail-closed.- Pinning de DNS: o host é resolvido uma única vez, todos os IPs devolvidos são validados e a
ligação é feita ao IP fixado, com o hostname original no
Hoste no SNI (o certificado TLS continua a ser validado contra o nome). Fecha a janela de DNS rebinding. - Cada salto de redirect repete resolução, validação e pinning. Num salto para outra origem
(esquema, host ou porta), só seguem os headers neutros, como o
Accepte oUser-Agent(NEUTRAL_HEADERS). OAuthorization, os cookies, os query params e qualquer header de credencial próprio, como umX-API-Keyou oX-Tableau-Auth, ficam para trás. - Teto da resposta e prazo total (cartões #571 e #605). O transporte dos conectores HTTP
(
pinned_httpx_transport) lê o corpo em streaming e para no teto do conector: 20 MB no REST e nos documentos do SharePoint e do Google Drive, 30 MB no OData e no Dynamics 365 e 32 MB quando o chamador não define o seu. UmContent-Lengthacima do teto é recusado antes do corpo. Só pedegzipedeflate, descomprime-os com a saída limitada ao mesmo teto e recusabrezstd. O timeout conta para o pedido inteiro, da ligação ao último byte, e não por leitura. - Aplica-se também às integrações de saída que recebem URLs do cliente (catálogo externo, OpenLineage, Git self-hosted): validação ao gravar e a cada uso.
- Fontes de dados (bases de dados, warehouses, object storage). Cada host e URL do config
volta à guarda (
netguard.check_host) antes de abrir a ligação, não só ao gravar a conexão (BaseConnector.assert_destination_allowed, cartão #339). O Postgres liga só aos IPs validados (hostaddr), pela ordem do resolver, e o MySQL abre o socket para eles, com o certificado verificado contra o nome. O pymongo resolve cada nome pela guarda (netguard.guarded_getaddrinfo, cartão #564) e liga aos endereços validados nessa mesma resolução, a monitorização do driver incluída, e nunca abre um socket Unix local, nem para um membro que o replica set anuncie (cartão #602). Os outros dialetos revalidam e o driver resolve o nome outra vez. Em produção, um nome que não resolve é recusado na criação da conexão e em cada revalidação, e os drivers com IP fixado recusam também um socket Unix local e uma lista de hosts. O quadro por dialeto está em CONECTORES.md. - Webhooks e tickets. Os webhooks dos canais de alertas (Slack, Teams e webhook genérico),
os relatórios agendados nesses canais e os tickets (Jira, Linear, ServiceNow, Azure DevOps,
GitHub e GitLab, cartão #376) saem todos por
app/infra/network/outbound.py. O URL é validado ao gravar, na app e na API de IaC, e de novo em cada envio, com o IP fixado. No webhook genérico, cada tentativa do retry volta a validar. Os redirecionamentos não são seguidos: um3xxconta como falha. Um destino recusado não se repete e aparece em «Última entrega» como «destino recusado (SSRF)». - O URL de um webhook é um segredo: quem o tiver publica no canal. Não aparece na resposta
do «Testar canal», em
last_delivery_errornem nos logs, que guardam só o código HTTP ou o tipo do erro. O logger dohttpx, que em INFO escreveria o URL de cada pedido, fica em WARNING. Os excertos das respostas dos fornecedores de tickets saem sem as credenciais. DATAVAULT_CONNECTOR_ALLOWED_HOSTSisenta também os webhooks e os tickets (por exemplo, um recetor na rede do cliente numa instância dedicada). Nos canais, ohttpscontinua obrigatório.
Testes: backend/tests/test_http_guard_pinning.py, backend/tests/test_dynamics_ssrf.py,
backend/tests/test_outbound_ssrf.py, backend/tests/test_connectors_sql.py,
backend/tests/test_security_gates.py, backend/tests/test_dns_rebinding_guard.py,
backend/tests/test_http_guard_response_limits.py.
Perímetro: WAF, rate limiting e DDoS
Estado: implementado como código e coberto por testes automáticos, mas ainda não aplicado
em produção. O load balancer global (deploy/lb_apply.sh) e o Cloud Armor por cima dele
(deploy/armor_apply.sh) esperam por uma operação da equipa. Até lá:
- a API e a app são servidas diretamente pelo Cloud Run, e o endereço
*.run.appde cada serviço também responde; - a contenção de abuso é o rate limit da própria API, em memória e por instância, mais o teto de instâncias do Cloud Run;
- não há WAF à frente do serviço.
A ativação é faseada. Primeiro entra o load balancer. Depois o Cloud Armor fica uma semana em modo de observação, em que nada é bloqueado e se medem os falsos positivos. Só então passa a bloquear. Esta secção só dirá «ativo» depois do bloqueio em produção.
Com o edge aplicado, o perímetro fica assim:
- Load balancer: todo o tráfego público (site, app, API e MCP) entra por um Application Load Balancer global da Google, com TLS ≥ 1.2. Os serviços passam a recusar pedidos que não venham dele. O LB só encaminha pedidos HTTP(S) bem formados, por isso os ataques volumétricos L3/L4 ficam na rede da Google. O Cloud Armor fica à frente dos backend services do LB.
- WAF com as regras OWASP CRS 3.3 geridas pela Google: SQLi, XSS, LFI, RFI, RCE, ataques ao
protocolo e deteção de scanners, na sensibilidade 1, a mais conservadora.
- Os endpoints que recebem SQL, YAML ou XML legítimos ficam fora do WAF: import de regras, conexões, manifest dbt, OpenLineage, SAML, webhooks assinados, entre outros. São autenticados e validados na aplicação.
- O WAF inspeciona os primeiros 8 KB do corpo de cada pedido.
- Rate limiting por IP na borda, com contadores centrais que não dependem do número de
instâncias:
- 600 pedidos/min por IP na API;
- 20 pedidos/min por IP no login, registo, recuperação de password, verificação de email e código MFA, com bloqueio de 10 min a quem passar. A força bruta distribuída fica contida antes de chegar à aplicação.
- Adaptive Protection: deteção de DDoS na camada 7 por machine learning, com alertas.
- Scanners e métodos:
TRACE,CONNECTe pedidos a/.env,/.gite/wp-*recebem 403. - Isenções: o rate limit só é saltado com um cabeçalho cujo valor está no Secret Manager (a validação de produção e os uptime checks), nunca por IP. O cabeçalho não salta o WAF.
- Monitorização: os bloqueios contam-se numa métrica sem amostragem. Acima de 200 em 5 min há alerta para a equipa.
- Defesa em profundidade: a API mantém o seu rate limit por IP, por token e por endpoint caro.
O MCP (
mcp.<domínio>) passa pelo mesmo LB sem Cloud Armor, porque os agentes saem por IPs partilhados do fornecedor. É autenticado por token e sujeito ao rate limit da API.
Testes: backend/tests/test_edge_armor.py (regras, isenções, idempotência, segredo nunca
impresso). Depois do bloqueio em produção, deploy/check_edge.sh prova três casos em cada deploy:
SQLi num query param → 403, path de scanner → 403, rajada ao login a partir de um IP → 429 em
menos de 10 s. Enquanto o Cloud Armor não bloquear, estes três casos aparecem no check só como
informação, sem passar nem falhar.
Cabeçalhos HTTP e teto de corpo da API
A API responde no seu próprio URL, sem passar pelo nginx da app, e é esse URL que o SDK, o MCP e
um scanner testam. Por isso envia os cabeçalhos de segurança ela própria
(backend/app/api/security_headers.py), em todas as respostas, incluindo os erros e os 429:
Strict-Transport-Security(um ano, com subdomínios),X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENYe umaPermissions-Policyque desliga câmara, microfone e localização;Cross-Origin-Resource-Policy: same-site, exceto nas rotas/api/v1/public/, que levamcross-originporque um badge entra como imagem em páginas de terceiros;- nas respostas JSON,
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; - num pedido com
Authorization,Cache-Control: no-store.
Um cabeçalho que a rota já definiu fica como está: o badge público mantém o seu Cache-Control e a
sua CSP, e os SSE o seu no-cache.
O corpo de um pedido tem um teto de 4 MB (api_max_body_bytes, em
backend/app/api/body_limit.py). Um Content-Length acima dele recebe 413 sem que a API leia o
corpo. Um corpo sem Content-Length é contado à medida que chega e a leitura para no teto. O 413
sai antes da autenticação e traz detail e max_bytes. As rotas que já tinham teto próprio
mantêm-no (ROUTE_BODY_LIMITS): manifest dbt, resultados dbt, OpenLineage, import de regras e
gate RAG. A validação de registos (POST /datasets/{id}/validate) tem 2 MB, por padrão de caminho
(ROUTE_BODY_LIMIT_PATTERNS).
A app envia os seus pelo nginx, com uma CSP própria da SPA (frontend/nginx/security-headers.conf).
A validação de produção (deploy/validate_prod.sh §17) verifica HSTS e nosniff no /health da
API, os seis cabeçalhos da app e, no site, HSTS, nosniff e a CSP. A CSP do site fica em
Report-Only até passar uma semana sem violações: o browser não bloqueia nada e envia cada violação
para /api/csp-report, que lê no máximo 16 KB por relatório e regista até 20 relatórios por IP em
cada 10 minutos. Testes: backend/tests/test_http_hardening.py.
RGPD: exportação e eliminação self-service
Sem pedido ao suporte:
- Exportar os dados do workspace —
GET /api/v1/workspace/export(administradores). - Apagar o workspace —
DELETE /api/v1/workspace: eliminação em cascata de conexões, datasets, perfis, scans, relatórios, integrações e credenciais (delete_workspace_cascade,app/domain/catalog/erasure.py). - Apagar a própria conta —
DELETE /api/v1/auth/me: anonimiza a autoria nos registos que têm de sobreviver (ex.: trilho de auditoria) e apaga o resto (erase_user). - Certificado de eliminação — apagar o workspace emite um certificado assinado (Ed25519, com a
data de emissão dentro do que é assinado). Conta as linhas de cada categoria de dados antes e
depois da cascata, na mesma transação, e diz até quando os backups guardam os dados apagados.
Fica no trilho da plataforma (
staff_events, sem emails) e segue por email para os admins, em JSON e PDF (app/domain/compliance/deletion_certificate.py). Verifica-se combackend/scripts/verify_deletion_certificate.py. - Retenção por workspace —
GET/PUT /api/v1/workspace/retention(administradores): prazos para o histórico de perfis (nunca acima da janela do plano), os relatórios IA e as amostras de linhas. A purga diária do worker aplica-os e deixaretention_purgedna Auditoria, com os cortes e as contagens (app/domain/billing/retention_policy.py,app/domain/billing/retention.py).
A política de privacidade e os termos estão publicados no site (/privacidade, /termos).
Analytics de produto: opt-in e sem dados pessoais
A app só mede que páginas e funcionalidades se usam quando um admin do workspace liga a partilha.
Vem desligada (analytics_enabled), e o admin responde uma vez depois do registo. Pode mudar a
escolha em Equipa & Acessos, onde está a lista exata do que se recolhe (GET/PUT /api/v1/workspace/analytics). Cada mudança fica na Auditoria (analytics_consent_changed).
- O que se grava — o workspace, o plano, o nome do evento, o modelo da página
(
/datasets/:datasetId, nunca o URL com ids) e a hora (product_events). Não se grava o utilizador, o email, o IP, o user-agent nem texto escrito. Os eventos, as rotas e as propriedades vêm de listas fechadas (app/domain/analytics/events.py). O que não está nelas é descartado. - Receção —
POST /api/v1/events/product, em lotes de até 50 eventos, com limite próprio de pedidos (product_events, 60 por minuto). Responde sempre 204: sem a partilha ligada não grava. - Prazo — 90 dias, pela purga diária (
app/domain/billing/retention.py). Desligar a partilha apaga logo os eventos do workspace (app/domain/analytics/service.py). Os eventos saem no export do workspace e na eliminação em cascata. - O que a BeryxTech vê — na Consola, só percentagens por plano e contagens por página dos últimos
30 dias. Uma célula com menos de 5 workspaces aparece sem números, e os workspaces da BeryxTech.
ficam de fora (
app/domain/analytics/adoption.py).
Rotulagem de conteúdo de IA (art. 50.º do AI Act)
Todo o conteúdo produzido com um modelo de linguagem — relatórios, sugestões, diagnósticos,
respostas do assistente — leva um rodapé que identifica que foi gerado com apoio de IA, qual
modelo e qual fornecedor responderam de facto, e que está sujeito a revisão humana, nos termos
do art. 50.º do Regulamento (UE) 2024/1689 (ai_generated_footer,
app/domain/insights/templates.py). O modo offline (sem IA) leva um rodapé próprio que o diz.
Marcação legível por máquina (art. 50.º, n.º 2). O texto gerado leva também três campos,
calculados num só sítio (app/domain/insights/ai_marking.py): ai_generated, ai_model e
generated_at. Saem:
- na API, nos relatórios e diagnósticos RCA, nas sugestões e no README do dataset (rascunho e versão publicada);
- no evento
donedo assistente, que também nomeia o modelo que respondeu; - no export HTML e na partilha pública, como
<meta name="ai-generated">,ai-modeleai-generated-at, com o rótulo visível «Gerado por IA (modelo, data)»; - nos metadados do PDF (
aigenerated,aimodel,aigeneratedat), no front matter doREADME.mdexportado e nos cartões Slack e Teams dos relatórios agendados (rótulo visível).
O texto do modo offline sai com ai_generated=false. O nome do modelo é limpo antes de sair: um
ARN ou um caminho de projeto da conta cloud do cliente fica reduzido à família do modelo ou a
undisclosed. No assistente, o painel diz desde a primeira interação que se fala com um sistema
de IA (art. 50.º, n.º 1). Testes: backend/tests/test_ai_marking.py e
frontend/src/pages/Assistente.dom.test.tsx.
Para clientes com sistemas de IA de risco elevado, o BeryX exporta também o dossiê de
evidência do art. 10.º (governação de dados de treino, validação e teste), com as lacunas
explícitas (app/domain/compliance/ai_act.py, teste backend/tests/test_ai_act_art10.py).
Ações da IA
O assistente do chat pode propor ações no workspace: criar uma regra, registar, atribuir ou adiar
um issue, pedir a revisão da classificação de PII, abrir um PR de remediação, entre outras.
Propor nunca executa. O utilizador vê um cartão com o que vai acontecer e só a aprovação o
executa, num endpoint que volta a verificar a assinatura do cartão (HMAC, válida 24 h), o papel, o
plano e a política, e grava o evento de auditoria assistant_action_executed.
Cada workspace tem uma política das ações da IA (Equipa → Ações da IA, só administradores). Por tipo de ação, decide:
- se está ligado. Desligado, sai das ferramentas do assistente e a execução é recusada;
- quem propõe e quem aprova (membro ou administrador). Quem aprova nunca fica abaixo do papel que a API exige para a mesma operação. Quem pode propor mas não aprovar envia um pedido, que um administrador aprova ou recusa. Os pedidos valem 7 dias;
- a auto-aprovação, só para os tipos marcados como seguros (adiar um issue, atribuir um issue a
si próprio) e desligada por omissão. Mesmo sem clique, a execução fica no trilho, marcada
auto_approved. A auto-aprovação também respeita os papéis por domínio (ADR 0011): só corre quando quem pede pode alterar o dataset do issue, a mesma verificação do «Aprovar». Num domínio restrito onde é leitor, ou que não vê, o cartão fica à espera do «Aprovar», que o recusa; - o limite diário no workspace (dia UTC). No limite, o assistente explica-o no chat e a aprovação responde 429 com o motivo;
- os destinos permitidos (por exemplo, os repositórios git), os campos obrigatórios e o modelo do corpo.
Leitores nunca recebem propostas e um token de API nunca aprova. Sem alterações, valem as
permissões da API, como antes da política existir. Cada alteração fica no registo de auditoria
(assistant_policy_changed, com o antes e o depois), tal como cada pedido
(assistant_action_requested) e cada recusa (assistant_action_request_rejected). O mesmo ecrã
decide o que o turno diário do steward pode propor e aplicar sozinho.
Implementação: app/domain/assistant/policy.py. Testes:
backend/tests/test_assistant_action_policy.py.
Endpoint público da app Slack
A app Slack do BeryX (cartão #193) é criada pelo cliente no workspace Slack dele, a partir de
um manifesto com três scopes de bot: chat:write, app_mentions:read e im:history. Recebe as
menções e as mensagens diretas ao bot em POST /api/v1/webhooks/slack/events/{integration_id},
um endpoint público, sem sessão nem token de API. A autenticação é a assinatura do Slack:
- Assinatura sobre o corpo cru. O endpoint verifica
X-Slack-Signature(v0=+ HMAC-SHA256 com o signing secret da integração sobrev0:{ts}:{raw_body}) em tempo constante, antes de interpretar o JSON (app/infra/collaboration/slack.py). Uma assinatura inválida recebe 401, e o desafiourl_verificationsó é respondido depois desta verificação. - Janela de replay de 5 minutos. Um
X-Slack-Request-Timestampa mais de 5 minutos do relógio do servidor recebe 401, mesmo com a assinatura certa. - Equipa, canal e dedupe. O
team_iddo evento tem de ser o que o Slack devolveu ao gravar a integração. Só passam menções e mensagens na DM com o bot, nunca de bots, e só nos canais que um administrador autorizou. Umevent_idrepetido não é processado duas vezes. - Tenant suspenso. Com o workspace suspenso pela BeryxTech, o endpoint não grava nem enfileira as perguntas, o worker fecha as que já estavam na fila sem chamar o modelo nem o Slack, e as publicações que esperavam envio ficam por enviar. É a mesma regra das sessões e dos tokens de API.
- Ack imediato. O endpoint grava a pergunta cifrada com Fernet, publica-a numa fila e responde
- O assistente corre no worker, só de leitura e com as permissões de quem ligou a conta Slack à conta BeryX. A pergunta é apagada quando o pedido fecha.
- Saída limitada. O cliente Slack só chama
chat.postMessage,chat.postEphemeraleauth.test, e recusa outro método antes de ir à rede. As publicações proativas levam só ids, contagens, nomes do catálogo, severidade e links, por uma lista fechada de chaves. Nas respostas do bot, a regra de não copiar valores nem amostras é uma instrução ao modelo. O filtro mecânico é o guardrail de saída da IA, que mascara emails, telefones, NIF/IBAN, cartões, IPs e segredos, e o escape do mrkdwn, para que nenhuma resposta notifique um canal inteiro. Outros valores de colunas só ficam de fora pela instrução.
Implementação: app/domain/collaboration/inbound.py. Guia de instalação:
SLACK_APP.md. Testes: backend/tests/test_slack_app.py.
Os teus modelos: chave própria, Amazon Bedrock e Vertex AI
- Chave Anthropic própria por workspace (cifrada como qualquer credencial).
- Política de IA por workspace: «IA desligada» (só heurísticas e templates offline) ou restrição de modelos por tarefa.
- Claude na conta cloud do cliente (plano Enterprise): Amazon Bedrock, só regiões
eu-*, viasts:AssumeRolecomExternalIdpor workspace — onboarding Bedrock; ou Google Cloud Vertex AI, só regiõeseurope-*, por impersonação de uma service account do cliente, sem chaves JSON — onboarding Vertex. Os dados e a fatura dos tokens ficam na conta do cliente; a BeryxTech não guarda segredos dessa rota. - Se a rota do cliente falhar, a IA degrada para os modos offline — nunca cai para a conta da BeryxTech contra a escolha do cliente.
Anonimização antes do LLM
- Por omissão, só a Anthropic recebe dados de clientes. Um fornecedor alternativo nunca recebe amostras em bruto: só metadados (nomes de colunas, tipos, contagens) ou, se o workspace o ativar expressamente, amostras já anonimizadas.
- As amostras enviadas a um modelo são limitadas em tamanho e podem ser anonimizadas antes de
sair: o motor (Microsoft Presidio, com reconhecedores portugueses para NIF, NISS, IBAN/NIB,
telefone e Cartão de Cidadão) substitui os valores por marcadores reversíveis só dentro da
sessão (
app/domain/llm/anonymize.py). - O encaminhamento recusa qualquer degrau não-Anthropic que não receba a amostra já anonimizada
(
app/domain/llm/routing.py).
Testes: backend/tests/test_llm_anonymize.py.
Limites de tentativas no início de sessão
O login por password tem três limites, em backend/app/api/ratelimit.py:
- Por IP e email: 5 tentativas por minuto.
- Por IP: 30 tentativas em 5 minutos, somadas entre o login, o registo e o pedido de recuperação de password. Trava o password spraying, em que uma password fraca é tentada em muitos emails a partir do mesmo IP.
- Por conta: 20 tentativas em 15 minutos, de qualquer IP. Trava a força bruta distribuída por muitos IPs contra a mesma conta. A chave é o sha256 do email e vale igual para emails sem conta. O pedido de recuperação de password tem um limite por conta à parte, com os mesmos números.
Os três verificam-se antes de a tentativa contar, por isso um 429 num deles não gasta os outros. Uma
entrada com a password certa não gasta nenhum. Acima do limite, a resposta é 429 com Retry-After e
a mesma mensagem, exista a conta ou não, e sai antes de a API procurar a conta. Quando o limite por
conta dispara, o registo de auditoria do workspace da conta recebe login_throttled, com o hash do
email e o IP, nunca a password, uma vez por conta em cada janela.
Quem sabe o email de uma conta consegue travar-lhe o login por password durante 15 minutos. O SSO e o pedido de recuperação de password continuam a funcionar nesse tempo.
Pela aplicação web, o limite por IP conta o IP do browser, que o servidor web atesta à API com uma chave partilhada. Em produção, a API não arranca sem essa chave. Fora de produção e sem ela, a API vê o mesmo IP em todos os pedidos da aplicação. Para que as falhas de uns utilizadores não travem o login aos outros, um pedido que se apresente como vindo do servidor web fica então fora do limite por IP. Os limites por IP e email e por conta aplicam-se na mesma. Em produção, a chave está ativa desde 11/10/2026.
Nos limites por IP da API, um endereço IPv6 conta pelo seu bloco /64 e um IPv4 pelo endereço completo. Quem tem um bloco IPv6 escolhe a parte final do endereço, e mudar de endereço dentro do bloco não abre um limite novo.
Os contadores ficam em memória em cada instância da API: com 3 instâncias, cada limite pode valer até o triplo. A contenção central, por IP real, é a do Cloud Armor (ver «Perímetro»), ainda por aplicar em produção.
Testes: backend/tests/test_security_gates.py e backend/tests/test_ipv6_rate_limit.py.
Sessões
Cada início de sessão fica registado com o dispositivo, o IP truncado (/24 em IPv4, /48 em IPv6),
a data de criação e o último uso; o token nunca é guardado (backend/app/domain/auth/sessions.py).
Em «Segurança da conta → Sessões ativas», cada pessoa vê onde tem sessão aberta, termina uma sessão
ou todas as outras. «Terminar sessão» acaba a sessão também no servidor.
- Inatividade. Uma sessão sem pedidos durante 12 horas termina. O administrador do workspace pode exigir menos, até 15 minutos, em «Equipa → Sessões». A mudança vale para as sessões já abertas. Um pedido feito sem interação há mais de um minuto não conta como atividade: é o caso das páginas com atualização ao vivo e da ligação do sino, que volta a abrir sozinha a cada 4 minutos.
- Logout forçado. O administrador termina todas as sessões de um membro em «Equipa». A pessoa pode voltar a entrar com as credenciais dela.
- Password e MFA. Redefinir a password, ativar ou desligar o MFA e regenerar os códigos de recuperação terminam todas as sessões da conta.
- Cada revogação, logout forçado, troca de password e mudança do timeout fica no registo de auditoria.
Tokens de API
Um token dvk_* só fica guardado como SHA-256, e o valor só aparece uma vez, ao criar ou ao rodar
o token (backend/app/domain/access/token_lifecycle.py).
- Prazo. Cada token novo expira: 90 dias por omissão, ou entre 1 e 365 dias, à escolha do
administrador. Um token expirado responde 401 em qualquer rota, com a mesma mensagem de um token
inválido. A instalação pode aceitar tokens sem prazo (
DATAVAULT_API_TOKEN_ALLOW_NO_EXPIRY); por omissão, não aceita. Os tokens criados antes desta regra não têm prazo e continuam a funcionar até o administrador os rodar. - Rotação. «Rodar», em «Equipa → Tokens de API», cria um token novo com o mesmo nome e o mesmo
scope. O antigo continua a funcionar durante o período de graça (24 horas por omissão), para se
trocar o valor na pipeline sem a partir. «Revogar» corta-o logo. A rotação fica no registo de
auditoria (
token_rotated). - Último uso. A lista de tokens mostra o último uso e o último IP de cada token, atualizados no máximo uma vez por minuto. O IP fica na base de dados e só o administrador o vê: não entra nos logs nem no registo de auditoria.
- Avisos. Um token ativo há mais de 180 dias gera um alerta para os administradores. O resumo semanal dos canais de notificação diz quantos tokens expiram nos 14 dias seguintes.
Testes: backend/tests/test_api_token_lifecycle.py.
Restrições de acesso da organização
O administrador da organização define, em «Equipa → Organização → Restrições de acesso», regras que
valem para todos os workspaces da organização (backend/app/domain/organizations/access_policy.py):
- Allowlist de IPs. Uma lista de IPs e blocos CIDR, IPv4 e IPv6. Com a lista preenchida, um IP de
fora é recusado no início de sessão (password, links de email, segundo passo do MFA e SSO), antes de
a sessão ser emitida, e em cada pedido autenticado: sessões abertas, tokens de API e o Copilot do
Microsoft 365, que chega da rede da Microsoft. O IP é o que o rate limit usa, por isso um
X-Forwarded-Forforjado não passa. Quem é recusado recebe uma mensagem com o IP visto e o que fazer; uma password errada continua a dar o erro de sempre, sem revelar a política. A sessão não termina: de volta a uma rede autorizada, continua. - Tokens de API isentos pelo nome. Um token com o nome indicado, no workspace indicado, entra de qualquer rede, por exemplo o CI com IPs dinâmicos. Cada isenção leva o motivo escrito pelo administrador. Rodar o token, ou seja criar outro com o mesmo nome e revogar o antigo, mantém a isenção. Um token com o mesmo nome noutro workspace não fica isento.
- Duração máxima das sessões. Entre 1 hora e a validade da instalação (12 horas por omissão), contada desde o início de sessão, com ou sem atividade. Baixar o valor termina já as sessões que o passaram; o ecrã de login diz porquê.
- MFA obrigatória em toda a organização. Quem entra com password tem de configurar o MFA no próximo início de sessão. Quem entra por SSO usa a MFA do fornecedor de identidade.
- Anti-bloqueio. Guardar uma lista que não inclui o IP de quem guarda é recusado. Se mesmo assim
todos ficarem de fora, por exemplo porque o IP do escritório mudou, o operador da BeryxTech desliga a
lista com
backend/scripts/clear_ip_allowlist.py, depois de confirmar o pedido com o cliente. - Cada mudança fica no registo de auditoria de todos os workspaces da organização. Cada recusa por IP fica no do workspace da conta ou do token, e a secção mostra as últimas 20 recusas da organização. Num pedido de uma sessão ou de um token já autenticados, fica uma recusa por conta, IP e origem em cada 15 minutos.
O acesso de suporte da BeryxTech continua a seguir o consentimento do cliente (ver Isolamento entre clientes) e não passa pela allowlist da organização do cliente.
Registo de auditoria
As ações de administração (papéis, planos, conexões, credenciais, tokens, sessões, política de IA, política
das ações da IA, acessos de suporte) e as ações do assistente aprovadas, pedidas ou recusadas ficam
num registo de auditoria gravado na mesma transação da mudança, consultável e exportável em CSV
pelos administradores do workspace. Cada evento fica 400 dias: uma purga diária do worker apaga os
mais antigos e deixa audit_log_purged no registo, com o corte e a contagem
(app/domain/audit/retention.py).
Subprocessadores e fornecedores
A lista de subprocessadores é pública em /subprocessadores (pt/en): finalidade, região, base
legal, mecanismo de transferência e DPA de cada um. Um fornecedor novo, uma saída ou uma mudança de
região é anunciada com 30 dias de antecedência. Os administradores podem receber o aviso por email
(Conformidade → Aviso de subprocessadores). Durante o pré-aviso, um fornecedor de IA novo não recebe
dados: o router só envia para subprocessadores ativos da lista. A avaliação de risco de cada
fornecedor (certificações declaradas, dados que recebe, plano de saída) está em
docs/compliance/vendors/.
Certificações: ISO 27001 e SOC 2
Não temos certificação ISO 27001 nem relatório SOC 2. Estão em programa: os controlos descritos neste documento são a base desse trabalho. Até lá, respondemos às linhas de RFP com a evidência técnica acima, e não com selos — e não usamos «conforme» nem «alinhado com» como substituto de uma certificação que não temos.
O estado público de cada certificação (planeada, em preparação, auditoria agendada, obtida — com
datas) vive num único ficheiro, docs/compliance/trust-status.json, que o site lê no build para a
página /datavault/seguranca e para o /.well-known/security.txt. O backend/tests/test_trust_status.py
falha se esse estado divergir desta secção ou do ADR 0010.
Um dos controlos já operacionais é a gestão de vulnerabilidades (compliance/vulnerability-management.md). Inclui:
- SCA das dependências Python e npm e scan das imagens com o Trivy em cada PR;
- falha do CI em critical/high sem exceção registada;
- SLA de 7, 30 e 90 dias por severidade;
- SBOM CycloneDX por deploy;
- um relatório mensal em
docs/compliance/evidence/; - DAST semanal (OWASP ZAP baseline, só passivo) ao site, à app e à API, com relatório semanal e o mesmo registo de exceções.
Os achados de todas as fontes (scanners, DAST, pen test externo e divulgação responsável, pelo
/.well-known/security.txt) seguem um processo com SLA
(compliance/security-findings.md). Ainda não fizemos um pen
test externo. O âmbito, as contas de teste e as regras estão preparados
(compliance/pentest.md); falta contratar o fornecedor.
O que não fazemos, e porquê
- Remediação destrutiva nas fontes. Propomos a correção (SQL/PySpark, ou um pull request no repositório do cliente); quem aplica é o cliente. Pedir credenciais de escrita seria a objeção n.º 1 de qualquer equipa de segurança — é uma decisão de produto, não uma limitação.
- MDM / golden record com escrita. Detetamos duplicados e propomos a consolidação, mas não mantemos um registo-mestre nem escrevemos a versão «vencedora» nas fontes.
- Análise multimodal de imagens. Os documentos são lidos pela camada de texto; imagens não são
processadas, e um PDF digitalizado sem texto é assinalado como a precisar de OCR
(
app/domain/quality/documents.py) em vez de ser enviado a um modelo de visão.