Existe uma diferença fundamental entre uma organização que é segura e uma organização que pode demonstrar que é segura. A primeira é uma propriedade técnica — medida por testes de penetração, scanning de vulnerabilidades e monitoramento de runtime. A segunda é uma propriedade regulatória e contratual — medida pela capacidade de produzir evidências documentadas de que controles específicos existem, funcionam e são auditados continuamente.
Frameworks como LGPD, SOC2 e ISO 27001 não exigem apenas que os controles existam — exigem que sua existência seja demonstrável. Para auditores, clientes corporativos e reguladores, a evidência de que um controle funcionou ontem é tão importante quanto o controle em si. Organizações que tratam compliance como uma atividade manual e episódica — coletando evidências às pressas antes de uma auditoria — pagam um preço alto em tempo de engenharia e risco de não conformidade.
A abordagem DevSecOps para compliance transforma esse processo: os mesmos pipelines que entregam software geram, armazenam e organizam evidências de conformidade continuamente. Quando uma auditoria chega, as evidências já existem — porque são um subproduto do trabalho de engenharia, não um esforço adicional.
Entendendo os Frameworks Relevantes
LGPD: Lei Geral de Proteção de Dados
A LGPD — Lei nº 13.709/2018 — é a lei brasileira de proteção de dados pessoais, inspirada no GDPR europeu. Seus impactos técnicos mais diretos sobre sistemas de software incluem:
Base legal para tratamento — toda operação sobre dados pessoais precisa de uma base legal documentada (consentimento, legítimo interesse, execução de contrato, etc.). Sistemas precisam registrar qual base legal justifica cada tipo de tratamento.
Direitos dos titulares — o sistema precisa implementar endpoints e processos que permitam ao titular acessar seus dados, corrigi-los, exportá-los em formato portável e solicitar sua exclusão. O prazo de atendimento é de 15 dias.
Notificação de incidentes — violações de dados que possam causar risco aos titulares devem ser notificadas à ANPD em até 72 horas. Isso exige sistemas de detecção rápida de vazamentos.
Data Protection by Design — a privacidade deve ser incorporada ao design dos sistemas, não adicionada depois. Minimização de dados, pseudonimização e criptografia são esperadas como padrão.
Registros de tratamento — o controlador deve manter um Registro das Atividades de Tratamento (ROPA — Records of Processing Activities) documentando quais dados são tratados, para qual finalidade, por quanto tempo e com quais medidas de segurança.
SOC2: Service Organization Control 2
O SOC2 é um framework de auditoria americano — mantido pelo AICPA — amplamente exigido por clientes corporativos B2B como evidência de que uma organização de software opera com controles adequados. É estruturado em torno de cinco Trust Service Criteria:
Security — proteção dos sistemas contra acesso não autorizado. É o único critério obrigatório em todos os relatórios SOC2.
Availability — os sistemas estão disponíveis conforme comprometido (SLAs). Requer monitoramento, gestão de incidentes e planos de recuperação.
Processing Integrity — o processamento de dados é completo, válido e autorizado. Relevante para sistemas de pagamento e processamento de dados críticos.
Confidentiality — dados designados como confidenciais são protegidos conforme acordado.
Privacy — dados pessoais são coletados, usados, retidos e descartados conforme a política de privacidade.
Um relatório SOC2 Type I avalia se os controles estão adequadamente desenhados em um ponto no tempo. Um SOC2 Type II — mais exigido por clientes enterprise — avalia se os controles funcionaram efetivamente durante um período (tipicamente 6 a 12 meses). É aqui que a geração contínua de evidências se torna essencial.
Mapeamento Técnico dos Controles SOC2
Os controles SOC2 do critério Security são organizados em categorias — CC1 a CC9. Os mais diretamente impactados por decisões técnicas de DevOps incluem:
CC6 — Logical and Physical Access Controls — controle de acesso baseado em princípio do menor privilégio, MFA para sistemas críticos, revisão periódica de acessos, offboarding imediato de usuários desligados.
CC7 — System Operations — monitoramento contínuo de sistemas, detecção e resposta a incidentes, gestão de vulnerabilidades, testes de controles.
CC8 — Change Management — processo documentado para todas as mudanças em produção, aprovações requeridas, testes antes do deploy, capacidade de rollback.
CC9 — Risk Mitigation — identificação e mitigação de riscos de fornecedores (supply chain), continuidade de negócios, planos de recuperação de desastres.
Implementando Logging de Auditoria Imutável
O log de auditoria é a espinha dorsal de qualquer programa de compliance — é a evidência de que ações específicas foram ou não realizadas. Para ser útil em auditorias, o log de auditoria precisa ter propriedades que o distinguem dos logs operacionais: imutabilidade, completude, integridade verificável e retenção de longo prazo.
Estrutura do Log de Auditoria
// src/audit/audit.service.js
const { EventBridge } = require('@aws-sdk/client-eventbridge');
const { CloudWatchLogs } = require('@aws-sdk/client-cloudwatch-logs');
const crypto = require('crypto');
class AuditService {
constructor() {
this.eventBridge = new EventBridge();
this.logGroupName = `/auditoria/${process.env.SERVICE_NAME}`;
this.previousHash = null;
}
// Gera um evento de auditoria com encadeamento de hash
// para garantir imutabilidade e detectar adulteração
async registrar(evento) {
const timestamp = new Date().toISOString();
const entradaAuditoria = {
// Identificação do evento
eventId: crypto.randomUUID(),
timestamp,
tipo: evento.tipo, // ex: 'usuario.login', 'dado.exportado'
resultado: evento.resultado, // 'sucesso' | 'falha' | 'negado'
// Quem realizou a ação
ator: {
id: evento.ator?.id,
email: evento.ator?.email,
tipo: evento.ator?.tipo, // 'usuario' | 'sistema' | 'api-key'
ip: evento.ator?.ip,
userAgent: evento.ator?.userAgent,
sessionId: evento.ator?.sessionId,
},
// O que foi afetado
recurso: {
tipo: evento.recurso?.tipo, // ex: 'usuario', 'pedido', 'relatorio'
id: evento.recurso?.id,
nome: evento.recurso?.nome,
},
// Detalhes adicionais da ação
detalhes: evento.detalhes || {},
// Contexto técnico
servico: process.env.SERVICE_NAME,
versao: process.env.APP_VERSION,
ambiente: process.env.NODE_ENV,
traceId: evento.traceId,
// Hash encadeado — liga cada entrada à anterior
// Permite detectar se entradas foram removidas ou alteradas
hashAnterior: this.previousHash,
};
// Calcula o hash desta entrada para encadear à próxima
const hashAtual = crypto
.createHash('sha256')
.update(JSON.stringify(entradaAuditoria))
.digest('hex');
entradaAuditoria.hash = hashAtual;
this.previousHash = hashAtual;
// Publica para EventBridge — roteado para múltiplos destinos
await this.eventBridge.putEvents({
Entries: [{
Source: `${process.env.SERVICE_NAME}.auditoria`,
DetailType: evento.tipo,
Detail: JSON.stringify(entradaAuditoria),
EventBusName: process.env.AUDIT_EVENT_BUS_NAME,
}],
});
return entradaAuditoria;
}
// Registra acesso a dados pessoais — obrigatório para LGPD
async registrarAcessoDadosPessoais(params) {
return this.registrar({
tipo: 'dados-pessoais.acesso',
resultado: params.resultado || 'sucesso',
ator: params.ator,
recurso: {
tipo: 'dados-pessoais',
id: params.titularId,
nome: params.categoria, // 'financeiro', 'saude', 'identificacao'
},
detalhes: {
finalidade: params.finalidade,
baseLegal: params.baseLegal,
camposAcessados: params.campos,
quantidadeRegistros: params.quantidade,
},
traceId: params.traceId,
});
}
// Registra mudanças em dados pessoais
async registrarAlteracaoDadosPessoais(params) {
return this.registrar({
tipo: 'dados-pessoais.alteracao',
resultado: params.resultado || 'sucesso',
ator: params.ator,
recurso: {
tipo: 'dados-pessoais',
id: params.titularId,
},
detalhes: {
camposAlterados: params.campos,
// Nunca loga os valores — apenas quais campos mudaram
motivacao: params.motivacao,
solicitacaoTitular: params.solicitacaoTitular || false,
},
traceId: params.traceId,
});
}
}
module.exports = new AuditService();
Infraestrutura de Auditoria na AWS
# auditoria.tf
# Event Bus dedicado para eventos de auditoria
resource "aws_cloudwatch_event_bus" "auditoria" {
name = "${var.project_name}-${var.environment}-auditoria"
tags = local.tags_comuns
}
# CloudWatch Log Group com retenção de 7 anos (LGPD + SOC2)
resource "aws_cloudwatch_log_group" "auditoria" {
name = "/auditoria/${var.project_name}/${var.environment}"
retention_in_days = 2557 # 7 anos em dias
kms_key_id = aws_kms_key.auditoria.arn
tags = local.tags_comuns
}
# Regra que roteia TODOS os eventos do bus de auditoria para o CloudWatch
resource "aws_cloudwatch_event_rule" "auditoria_para_cloudwatch" {
name = "auditoria-todos-eventos"
event_bus_name = aws_cloudwatch_event_bus.auditoria.name
event_pattern = jsonencode({ source = [{ prefix = "" }] })
tags = local.tags_comuns
}
resource "aws_cloudwatch_event_target" "auditoria_cloudwatch" {
rule = aws_cloudwatch_event_rule.auditoria_para_cloudwatch.name
event_bus_name = aws_cloudwatch_event_bus.auditoria.name
target_id = "cloudwatch-logs"
arn = aws_cloudwatch_log_group.auditoria.arn
}
# Bucket S3 para arquivo de longo prazo com Object Lock
# Object Lock garante imutabilidade — nem administradores podem deletar
resource "aws_s3_bucket" "auditoria_arquivo" {
bucket = "${var.project_name}-${var.environment}-auditoria-arquivo"
# Object Lock deve ser habilitado na criação — não pode ser ativado depois
object_lock_enabled = true
tags = local.tags_comuns
}
resource "aws_s3_bucket_object_lock_configuration" "auditoria" {
bucket = aws_s3_bucket.auditoria_arquivo.id
rule {
default_retention {
mode = "COMPLIANCE" # COMPLIANCE = nem o root pode deletar
years = 7
}
}
}
resource "aws_s3_bucket_versioning" "auditoria" {
bucket = aws_s3_bucket.auditoria_arquivo.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "auditoria" {
bucket = aws_s3_bucket.auditoria_arquivo.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.auditoria.arn
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_public_access_block" "auditoria" {
bucket = aws_s3_bucket.auditoria_arquivo.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# CloudTrail — registra todas as chamadas de API da AWS
resource "aws_cloudtrail" "principal" {
name = "${var.project_name}-${var.environment}-trail"
s3_bucket_name = aws_s3_bucket.auditoria_arquivo.id
s3_key_prefix = "cloudtrail"
include_global_service_events = true
is_multi_region_trail = true
enable_log_file_validation = true # Detecta adulteração nos logs
kms_key_id = aws_kms_key.auditoria.arn
cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail.arn
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::"] # Todos os buckets S3
}
data_resource {
type = "AWS::Lambda::Function"
values = ["arn:aws:lambda"] # Todas as funções Lambda
}
}
tags = local.tags_comuns
}
resource "aws_cloudwatch_log_group" "cloudtrail" {
name = "/cloudtrail/${var.project_name}/${var.environment}"
retention_in_days = 2557
kms_key_id = aws_kms_key.auditoria.arn
tags = local.tags_comuns
}
Implementando os Direitos dos Titulares (LGPD)
// src/lgpd/titular.controller.js
const auditoria = require('../audit/audit.service');
const titularService = require('./titular.service');
class TitularController {
// Art. 18, I — Direito de acesso
async exportarMeusDados(req, res) {
const { titularId } = req.params;
// Verifica que o usuário autenticado é o próprio titular
if (req.usuario.id !== titularId) {
await auditoria.registrar({
tipo: 'lgpd.acesso-dados.negado',
resultado: 'negado',
ator: { id: req.usuario.id, ip: req.ip },
recurso: { tipo: 'dados-pessoais', id: titularId },
detalhes: { motivo: 'titular-diferente-do-solicitante' },
traceId: req.traceId,
});
return res.status(403).json({ erro: 'Acesso negado' });
}
const dados = await titularService.coletarTodosDados(titularId);
await auditoria.registrarAcessoDadosPessoais({
ator: { id: req.usuario.id, ip: req.ip },
titularId,
categoria: 'todos',
finalidade: 'portabilidade-lgpd-art18',
baseLegal: 'exercicio-de-direitos',
campos: Object.keys(dados),
quantidade: 1,
traceId: req.traceId,
});
// Retorna em formato legível e portável (JSON)
res.setHeader('Content-Disposition',
`attachment; filename="meus-dados-${titularId}-${Date.now()}.json"`);
res.json({
exportadoEm: new Date().toISOString(),
titular: titularId,
dados,
aviso: 'Exportação gerada em atendimento ao Art. 18, V da LGPD',
});
}
// Art. 18, VI — Direito de exclusão
async solicitarExclusao(req, res) {
const { titularId } = req.params;
const { motivo } = req.body;
if (req.usuario.id !== titularId) {
return res.status(403).json({ erro: 'Acesso negado' });
}
// Cria solicitação com prazo de 15 dias (Art. 19)
const solicitacao = await titularService.criarSolicitacaoExclusao({
titularId,
motivo,
prazoAtendimento: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000),
solicitadoPor: req.usuario.id,
});
await auditoria.registrar({
tipo: 'lgpd.exclusao-dados.solicitada',
resultado: 'sucesso',
ator: { id: req.usuario.id, ip: req.ip },
recurso: { tipo: 'dados-pessoais', id: titularId },
detalhes: {
solicitacaoId: solicitacao.id,
motivo,
prazoAtendimento: solicitacao.prazoAtendimento,
},
traceId: req.traceId,
});
res.json({
solicitacaoId: solicitacao.id,
prazoAtendimento: solicitacao.prazoAtendimento,
mensagem: 'Solicitação recebida. Você será notificado por email quando concluída.',
});
}
// Processo de anonimização para exclusão (Art. 18, VI)
async processarExclusao(solicitacaoId) {
const solicitacao = await titularService
.buscarSolicitacao(solicitacaoId);
// Anonimiza — não deleta, pois pode haver obrigação legal de retenção
// Ex: dados fiscais devem ser mantidos por 5 anos
await titularService.anonimizarDados(solicitacao.titularId, {
manterParaObrigacaoLegal: ['dados-fiscais', 'logs-transacao'],
});
await auditoria.registrar({
tipo: 'lgpd.exclusao-dados.concluida',
resultado: 'sucesso',
ator: { id: 'sistema', tipo: 'sistema' },
recurso: { tipo: 'dados-pessoais', id: solicitacao.titularId },
detalhes: {
solicitacaoId,
dadosAnonimizados: true,
dadosRetidosPorObrigacaoLegal: ['dados-fiscais', 'logs-transacao'],
baseRetencao: 'Decreto 9.580/2018 art. 279 — obrigação contábil 5 anos',
},
traceId: crypto.randomUUID(),
});
}
}
Geração Automatizada de Evidências para SOC2
Para SOC2 Type II, a evidência precisa demonstrar que controles funcionaram ao longo do tempo — não apenas que existem. A coleta pode ser automatizada como parte do pipeline:
#!/bin/bash
# scripts/coletar-evidencias-soc2.sh
# Executa mensalmente via GitHub Actions para coletar evidências SOC2
# Resultados armazenados no S3 com Object Lock para imutabilidade
set -euo pipefail
PERIODO="${1:-$(date +%Y-%m)}"
BUCKET="${SOC2_EVIDENCIAS_BUCKET}"
PREFIXO="soc2/${PERIODO}"
REGIAO="us-east-1"
log() { echo "[$(date -u +%H:%M:%S)] $*"; }
# ── CC6: Controle de Acesso ───────────────────────
log "Coletando evidências CC6 — Controle de Acesso..."
# Lista usuários IAM com acesso à conta
aws iam get-credential-report --query 'Content' --output text \
| base64 -d \
> /tmp/iam-credential-report.csv
aws s3 cp /tmp/iam-credential-report.csv \
"s3://${BUCKET}/${PREFIXO}/cc6/iam-credential-report.csv"
# Verifica políticas MFA
aws iam list-users --output json \
| jq '.Users[] | {
usuario: .UserName,
criado: .CreateDate,
ultimo_acesso: .PasswordLastUsed
}' \
> /tmp/usuarios-iam.json
# Verifica quais usuários têm MFA habilitado
while IFS= read -r usuario; do
MFA=$(aws iam list-mfa-devices \
--user-name "$usuario" \
--query 'MFADevices | length(@)')
echo "{\"usuario\": \"$usuario\", \"mfa_habilitado\": $([ "$MFA" -gt 0 ] && echo true || echo false)}"
done < <(aws iam list-users --query 'Users[].UserName' --output text | tr '\t' '\n') \
> /tmp/mfa-status.json
aws s3 cp /tmp/mfa-status.json \
"s3://${BUCKET}/${PREFIXO}/cc6/mfa-status.json"
# ── CC7: Operações do Sistema ─────────────────────
log "Coletando evidências CC7 — Operações do Sistema..."
# Métricas de disponibilidade do mês
aws cloudwatch get-metric-statistics \
--namespace AWS/ApplicationELB \
--metric-name HTTPCode_ELB_5XX_Count \
--dimensions Name=LoadBalancer,Value="${ALB_NAME}" \
--start-time "${PERIODO}-01T00:00:00Z" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 86400 \
--statistics Sum \
--output json \
> /tmp/erros-alb.json
aws s3 cp /tmp/erros-alb.json \
"s3://${BUCKET}/${PREFIXO}/cc7/metricas-disponibilidade.json"
# Relatório de vulnerabilidades identificadas e remediadas
aws securityhub get-findings \
--filters '{
"CreatedAt": [{"Start": "'"${PERIODO}"'-01T00:00:00Z",
"End": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}],
"RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}]
}' \
--query 'Findings[*].{
Titulo: Title,
Severidade: Severity.Label,
Status: Workflow.Status,
Criado: CreatedAt
}' \
--output json \
> /tmp/findings-seguranca.json
aws s3 cp /tmp/findings-seguranca.json \
"s3://${BUCKET}/${PREFIXO}/cc7/vulnerabilidades-identificadas.json"
# ── CC8: Gestão de Mudanças ───────────────────────
log "Coletando evidências CC8 — Gestão de Mudanças..."
# Histórico de deploys do mês via GitHub API
curl -s \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/deployments?per_page=100" \
| jq "[.[] | select(.created_at | startswith(\"${PERIODO}\"))]" \
> /tmp/deployments-mes.json
aws s3 cp /tmp/deployments-mes.json \
"s3://${BUCKET}/${PREFIXO}/cc8/deployments.json"
# PRs mergeados no período — evidência de revisão de código
curl -s \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
"https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/pulls?state=closed&per_page=100" \
| jq "[.[] | select(.merged_at != null and (.merged_at | startswith(\"${PERIODO}\")))]
| map({
numero: .number,
titulo: .title,
autor: .user.login,
revisores: [.requested_reviewers[].login],
mergeado_por: .merged_by.login,
mergeado_em: .merged_at
})" \
> /tmp/prs-mergeados.json
aws s3 cp /tmp/prs-mergeados.json \
"s3://${BUCKET}/${PREFIXO}/cc8/pull-requests-revisados.json"
# ── Gera índice consolidado ───────────────────────
log "Gerando índice de evidências..."
cat > /tmp/indice.json << EOF
{
"periodo": "${PERIODO}",
"gerado_em": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"gerado_por": "pipeline-automatizado",
"evidencias": {
"CC6": {
"descricao": "Controle de acesso e gestão de identidades",
"arquivos": [
"${PREFIXO}/cc6/iam-credential-report.csv",
"${PREFIXO}/cc6/mfa-status.json"
]
},
"CC7": {
"descricao": "Operações do sistema e gestão de vulnerabilidades",
"arquivos": [
"${PREFIXO}/cc7/metricas-disponibilidade.json",
"${PREFIXO}/cc7/vulnerabilidades-identificadas.json"
]
},
"CC8": {
"descricao": "Gestão de mudanças e revisão de código",
"arquivos": [
"${PREFIXO}/cc8/deployments.json",
"${PREFIXO}/cc8/pull-requests-revisados.json"
]
}
}
}
EOF
aws s3 cp /tmp/indice.json \
"s3://${BUCKET}/${PREFIXO}/indice.json"
log "Coleta de evidências concluída para o período ${PERIODO}"
Automatizando a coleta mensalmente:
# .github/workflows/coletar-evidencias-soc2.yml
name: Coleta Mensal de Evidências SOC2
on:
schedule:
- cron: '0 6 1 * *' # Primeiro dia de cada mês às 6h UTC
workflow_dispatch:
inputs:
periodo:
description: 'Período no formato YYYY-MM'
required: false
jobs:
coletar:
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_COMPLIANCE_ROLE_ARN }}
aws-region: us-east-1
- name: Coleta evidências SOC2
run: |
PERIODO="${{ inputs.periodo || '' }}"
if [ -z "$PERIODO" ]; then
PERIODO=$(date -d "last month" +%Y-%m 2>/dev/null || \
date -v-1m +%Y-%m)
fi
bash scripts/coletar-evidencias-soc2.sh "$PERIODO"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ORG: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
ALB_NAME: ${{ vars.ALB_NAME }}
SOC2_EVIDENCIAS_BUCKET: ${{ vars.SOC2_EVIDENCIAS_BUCKET }}
- name: Notifica time de compliance
if: success()
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: ${{ vars.SLACK_COMPLIANCE_CHANNEL }}
slack-message: |
✅ *Evidências SOC2 coletadas automaticamente*
Período: ${{ inputs.periodo || 'mês anterior' }}
Bucket: ${{ vars.SOC2_EVIDENCIAS_BUCKET }}
Workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
Registro de Atividades de Tratamento (ROPA)
O ROPA é obrigação direta da LGPD para o controlador. Na prática, é um documento vivo que descreve cada operação de tratamento de dados pessoais. A infraestrutura pode automatizar a geração de parte desse documento:
// src/lgpd/ropa.js
// Registro das Atividades de Tratamento — Art. 37 LGPD
const ROPA = [
{
id: 'cadastro-usuarios',
nome: 'Cadastro e autenticação de usuários',
controlador: 'Empresa S.A. — CNPJ XX.XXX.XXX/0001-XX',
encarregado: 'dpo@empresa.com',
finalidade: 'Criação e gestão de contas de usuário para acesso à plataforma',
baseLegal: 'Execução de contrato — Art. 7º, V, LGPD',
categoriaTitulares: 'Clientes e usuários cadastrados',
categoriasDados: [
{ dado: 'Nome completo', sensivel: false },
{ dado: 'E-mail', sensivel: false },
{ dado: 'CPF', sensivel: false },
{ dado: 'Senha (hash bcrypt)', sensivel: false },
{ dado: 'Data de nascimento', sensivel: false },
{ dado: 'IP de acesso', sensivel: false },
],
prazoRetencao: '5 anos após encerramento da conta (obrigação legal fiscal)',
criterioRetencao: 'Decreto 9.580/2018 — obrigação contábil',
compartilhamento: [
{
receptor: 'AWS (processador)',
pais: 'Brasil (us-east-1 com mecanismos adequados)',
finalidade: 'Hospedagem e processamento de dados',
garantia: 'DPA com AWS — cláusulas contratuais padrão',
},
],
medidasSeguranca: [
'Criptografia em trânsito (TLS 1.2+)',
'Criptografia em repouso (AES-256 via AWS KMS)',
'Controle de acesso baseado em papéis (RBAC)',
'Autenticação multifator para administradores',
'Logs de auditoria de todos os acessos',
],
avaliacaoRisco: 'Baixo — dados não sensíveis, processamento comum',
dataUltimaRevisao: '2025-01-15',
proximaRevisao: '2026-01-15',
},
{
id: 'processamento-pagamentos',
nome: 'Processamento de pagamentos',
baseLegal: 'Execução de contrato — Art. 7º, V, LGPD',
categoriasDados: [
{ dado: 'Dados do cartão (tokenizados — não armazenados)', sensivel: false },
{ dado: 'Valor da transação', sensivel: false },
{ dado: 'Histórico de compras', sensivel: false },
],
compartilhamento: [
{
receptor: 'Gateway de Pagamento XYZ',
pais: 'Brasil',
finalidade: 'Processamento de transações financeiras',
garantia: 'Contrato com cláusulas de proteção de dados',
},
],
prazoRetencao: '10 anos (obrigação legal — Banco Central do Brasil)',
avaliacaoRisco: 'Médio — dados financeiros, processador regulado pelo BACEN',
dataUltimaRevisao: '2025-01-15',
},
];
module.exports = { ROPA };
O Que Vem a Seguir
O próximo artigo cobre o tema de Performance e Otimização de Custos em Cloud — como identificar gargalos de performance em produção, como aplicar práticas de FinOps para controlar e otimizar os custos AWS, e como construir dashboards de custo que conectam gastos de infraestrutura ao valor entregue.
Referências para Aprofundamento
LGPD e regulação brasileira
- Lei Geral de Proteção de Dados — planalto.gov.br — Texto integral da LGPD com todas as alterações, incluindo direitos dos titulares, bases legais e obrigações do controlador.
- ANPD — Guias e Orientações — gov.br — Guias oficiais da Autoridade Nacional de Proteção de Dados sobre implementação da LGPD, incluindo orientações sobre ROPA e notificação de incidentes.
SOC2 e frameworks internacionais
- AICPA SOC2 Overview — aicpa-cima.org — Visão geral oficial do SOC2 pelo AICPA, incluindo os Trust Service Criteria e diferença entre Type I e Type II.
- AWS Compliance Center — aws.amazon.com — Centro de compliance da AWS com relatórios SOC2, ISO 27001, PCI-DSS e outros frameworks, incluindo o AWS Shared Responsibility Model.
Ferramentas
- AWS CloudTrail Documentation — docs.aws.amazon.com — Documentação completa do CloudTrail com guia de configuração de trilhas multi-região, validação de integridade de logs e integração com CloudWatch.
- AWS Security Hub — docs.aws.amazon.com — Documentação do Security Hub, incluindo os padrões de segurança disponíveis (AWS Foundational Security Best Practices, CIS, PCI-DSS) e integração com GuardDuty e Inspector.
Exercícios
Exercício 1
O AuditService encadeia as entradas por hash: cada registro carrega o hashAnterior, "permitindo detectar se entradas foram removidas ou alteradas". Onde this.previousHash está armazenado? O que acontece com a cadeia quando o Deployment roda com replicas: 3 e um dos pods é reiniciado?
Ver resposta
✓ Resposta: this.previousHash é uma propriedade de instância, em memória do processo — inicializada como null no construtor. Isso desmonta a garantia inteira, por duas razões independentes.
Com 3 réplicas, existem três instâncias do serviço, cada uma com sua própria variável. Não há uma cadeia: há três cadeias paralelas e independentes, entrelaçadas no destino final conforme o load balancer distribui as requisições. Uma entrada gerada no pod A aponta para o hash da entrada anterior daquele pod, não da anterior no tempo. Verificar a sequência no CloudWatch é impossível — os elos não formam uma linha.
E a cada reinício — rollout, OOMKill, consolidação de nó pelo Karpenter — o previousHash volta a null. Uma entrada com hashAnterior: null é indistinguível do início legítimo de uma cadeia. Um invasor que apagasse um bloco de registros só precisaria deixar o primeiro sobrevivente com hashAnterior: null para que a remoção passasse como "o serviço reiniciou aqui".
Encadeamento de hash exige um ponto serializador com estado durável: o hash anterior precisa vir de um armazenamento compartilhado, lido e gravado atomicamente (um contador no DynamoDB com escrita condicional, por exemplo). Só que serializar toda a auditoria em um único ponto é caro e vira gargalo — motivo pelo qual, na prática, a imutabilidade costuma ser delegada à infraestrutura: S3 Object Lock em modo COMPLIANCE, CloudWatch Logs com política que não permite DeleteLogGroup, e o enable_log_file_validation do CloudTrail — todos já presentes no auditoria.tf. O encadeamento em memória, como está, é teatro de segurança: parece uma prova e não é. Em auditoria isso é pior que não ter, porque cria confiança indevida.
Exercício 2
O bucket de arquivo usa Object Lock em modo COMPLIANCE com years = 7, e o comentário registra: "nem o root pode deletar". Um titular exerce o direito de exclusão do Art. 18, VI da LGPD, e seus dados pessoais estão dentro das entradas de auditoria já arquivadas nesse bucket. Como conciliar as duas exigências?
Ver resposta
✓ Resposta: Não se concilia depois — a decisão precisa ter sido tomada antes de o dado entrar no arquivo. Em modo COMPLIANCE o objeto é literalmente indelével pelos 7 anos: nem o usuário root da conta, nem a AWS a pedido do cliente, conseguem remover ou encurtar a retenção. Não existe recurso técnico para atender à exclusão naquele bucket.
A saída é de design, e o próprio AuditService já aponta o caminho quando comenta "nunca loga os valores — apenas quais campos mudaram". O log de auditoria deve conter identificadores, não conteúdo: titularId, tipo de recurso, categoria de dado, finalidade, base legal. Nada de nome, CPF, e-mail ou valores de campo. Assim, quando os dados do titular forem anonimizados na base operacional, o titularId remanescente no arquivo passa a ser um identificador sem sujeito ao qual se ligar — o registro prova que uma operação ocorreu sem revelar sobre quem.
Repare que a implementação atual não segue a própria regra: o objeto ator guarda email, ip e userAgent — dados pessoais do operador, e o IP é dado pessoal também segundo a ANPD. Esses campos entram no arquivo imutável de 7 anos.
Há ainda um contraponto jurídico que importa: o direito de exclusão não é absoluto. O Art. 16 permite a conservação para cumprimento de obrigação legal ou regulatória, e é exatamente essa a base sobre a qual o log de auditoria se sustenta — o mesmo raciocínio que o método processarExclusao aplica ao reter dados fiscais. O que a LGPD exige é que essa retenção seja justificada, documentada no ROPA e limitada ao mínimo necessário. Guardar o CPF do titular por 7 anos "porque o log é imutável" não passa nesse teste; guardar o titularId passa.
Nota operacional sobre o COMPLIANCE: ele também torna o custo irreversível. Um bug que despeje volume anômalo de eventos nesse bucket gera armazenamento que ninguém poderá apagar por sete anos. O modo GOVERNANCE, que permite override por um principal com permissão específica e registra esse override no CloudTrail, é a escolha adequada quando não há exigência formal do modo mais rígido.
Exercício 3
O script coleta a evidência de CC8 — revisão de código — extraindo revisores: [.requested_reviewers[].login] dos PRs mergeados. Na auditoria, o auditor observa que a maioria dos PRs aparece com "revisores": [], apesar de a proteção de branch exigir uma aprovação. O que aconteceu?
Ver resposta
✓ Resposta: O campo está medindo a coisa errada. requested_reviewers lista quem foi convidado a revisar e ainda não revisou — é uma fila de pendências, não um registro de aprovações. Quando a pessoa submete a review, o GitHub a remove dessa lista. O resultado é perfeitamente invertido: PRs devidamente revisados e aprovados aparecem com o array vazio, e um array preenchido indicaria justamente uma revisão pendente.
É o pior tipo de falha em compliance — não é um erro que quebra o pipeline, é uma evidência que se coleta com sucesso, se arquiva no bucket imutável e se apresenta ao auditor durante meses, provando o oposto do que se queria provar. E como o Object Lock em COMPLIANCE impede correção retroativa, os arquivos errados ficam lá.
A aprovação vive em outro endpoint — /pulls/{n}/reviews — onde interessam as entradas com state: "APPROVED":
for PR in $(jq -r '.[].number' /tmp/prs-mergeados.json); do
curl -s -H "Authorization: Bearer ${GITHUB_TOKEN}" \
"https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/pulls/${PR}/reviews" \
| jq --arg pr "$PR" '[.[] | select(.state == "APPROVED")
| {pr: $pr, aprovador: .user.login, em: .submitted_at}]'
done | jq -s 'add' > /tmp/aprovacoes.json
Dois cuidados adicionais no mesmo script. A busca de PRs usa per_page=100 sem paginação e filtra o período depois de receber a página: num repositório ativo, os PRs do mês podem simplesmente não estar entre os 100 mais recentes, e a evidência sai incompleta sem nenhum erro — o correto é paginar com Link: rel="next", ou usar a Search API filtrando por merged:YYYY-MM-01..YYYY-MM-31. E vale coletar também a configuração da proteção de branch (/branches/main/protection): ela é a evidência de que a regra existia no período, complementando a lista de quem aprovou o quê.
A lição vale para todo o script: evidência automatizada precisa ser verificada contra um caso conhecido antes de entrar em produção. Rodar a coleta em um PR que se sabe aprovado, e conferir se o aprovador aparece, custa cinco minutos e evita meses de arquivo inútil.
Exercício 4
O método processarExclusao anonimiza em vez de deletar, e o comentário justifica: "não deleta, pois pode haver obrigação legal de retenção". Isso é conformidade legítima ou uma forma de contornar o direito do titular? O que a anonimização precisa cumprir para ser aceita?
Ver resposta
✓ Resposta: É legítimo — e é o mecanismo previsto na própria lei —, desde que a anonimização seja real. O Art. 12 estabelece que dados anonimizados não são dados pessoais e, portanto, saem do escopo da LGPD. É o que permite reter a informação necessária a obrigações contábeis e fiscais sem manter o titular identificável.
O critério está no Art. 5º, XI: a anonimização deve tornar o titular não identificável, considerando meios técnicos razoáveis e disponíveis na ocasião do tratamento. E o mesmo artigo traz a virada: se o processo puder ser revertido, o dado é apenas pseudonimizado — continua sendo dado pessoal, continua integralmente sujeito à lei, e a exclusão não foi atendida.
Na prática, três coisas separam uma anonimização de um teatro de anonimização. Irreversibilidade: substituir o CPF pelo seu hash não anonimiza nada — o espaço de CPFs é pequeno o bastante para ser varrido por força bruta em minutos, então o hash reidentifica. Resistência a cruzamento: apagar o nome e manter CEP, data de nascimento e gênero deixa boa parte da população brasileira identificável pela combinação; um titularId que ainda referencie a tabela de pedidos com endereço de entrega não anonimizou coisa alguma. E consistência entre sistemas: anonimizar no banco principal enquanto o data warehouse, o backup, o índice de busca e a ferramenta de analytics mantêm a cópia original é o modo mais comum de a operação falhar — a base de fato passa a ser o conjunto de lugares onde o dado não foi anonimizado.
Sobre o código em si, dois problemas concretos. A baseRetencao registrada é "Lei 9.394/1996 art. 37" — a Lei 9.394/1996 é a LDB, a lei de diretrizes e bases da educação, que não tem relação com obrigação contábil; o ROPA, no mesmo artigo, cita corretamente o Decreto 9.580/2018. Uma citação legal errada dentro do registro imutável de auditoria é exatamente o tipo de detalhe que um auditor encontra. E manterParaObrigacaoLegal: ['logs-transacao'] é vago demais para se sustentar: quais campos do log, por qual prazo, sob qual dispositivo. Retenção por obrigação legal exige o dispositivo específico e o prazo dele — não uma categoria genérica.
Exercício 5
Os dois log groups usam retention_in_days = 2557, comentado como "7 anos em dias". Mas 7 × 365 = 2555. O valor está errado por dois dias? E o que o enable_log_file_validation = true do CloudTrail acrescenta que o Object Lock do bucket já não garante?
Ver resposta
✓ Resposta: O valor está certo. O retention_in_days do CloudWatch Logs não aceita qualquer número — é uma enumeração fechada de valores permitidos (1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653). Escrever 2555 faria o terraform apply falhar com InvalidParameterException. Os valores longos seguem anos de 365,25 dias para acomodar bissextos: 2557 é o degrau de 7 anos, assim como 1827 é o de 5 e 3653 o de 10.
Quanto às duas proteções, elas cobrem ameaças diferentes e se complementam:
O Object Lock protege contra remoção — impede que alguém apague ou sobrescreva o objeto no S3. É controle de acesso ao armazenamento.
O log file validation protege contra adulteração, e opera fora do S3. O CloudTrail calcula o hash SHA-256 de cada arquivo entregue e publica periodicamente um digest file assinado com uma chave privada da AWS, encadeando cada digest ao anterior. Com o comando aws cloudtrail validate-logs, qualquer pessoa verifica se os arquivos de um intervalo continuam íntegros — e, crucialmente, detecta arquivos que deveriam existir e não estão lá.
A diferença importa porque o Object Lock protege o objeto, não o conjunto. Ele nada diz sobre um arquivo que nunca chegou a ser gravado — porque o CloudTrail foi desligado por vinte minutos e religado, ou porque a entrega falhou. O digest encadeado torna essa lacuna visível. É a mesma propriedade que o AuditService tentava obter com o hashAnterior do exercício 1, só que aqui implementada por quem tem o serializador durável para fazê-la valer.
Vale notar uma tensão de custo entre as duas configurações: o event_selector registra data_resource para todos os objetos S3 e todas as funções Lambda. Data events são cobrados por evento e são, de longe, o maior gerador de volume do CloudTrail — uma aplicação com tráfego intenso no S3 pode produzir dezenas de gigabytes por dia, e cada byte cai em um bucket que ninguém poderá apagar pelos próximos sete anos.