Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 0 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,35 +66,25 @@ Cria múltiplos certificados a partir de uma lista de dados recebida diretamente
"first_name": "João",
"last_name": "Silva",
"email": "joao.silva@example.com",
"phone": "(48) 99999-9999",
"cpf": "123.456.789-00",
"city": "Florianópolis",
"product_id": 5678,
"product_name": "Workshop de Python Avançado",
"certificate_details": "In recognition of their participation in the Workshop de Python Avançado, held on January 15, 2025, at IFSC – Câmpus Florianópolis, Brazil, with a total duration of 8 hours.",
"certificate_logo": "https://example.com/logo.png",
"certificate_background": "https://example.com/background.png",
"order_date": "2025-01-10 14:30:00",
"checkin_latitude": "-27.5667",
"checkin_longitude": "-48.5156",
"time_checkin": "2025-01-15 09:00:00"
},
{
"order_id": 1235,
"first_name": "Maria",
"last_name": "Santos",
"email": "maria.santos@example.com",
"phone": "(48) 88888-8888",
"cpf": "987.654.321-00",
"city": "São José",
"product_id": 5678,
"product_name": "Workshop de Python Avançado",
"certificate_details": "In recognition of their participation in the Workshop de Python Avançado, held on January 15, 2025, at IFSC – Câmpus Florianópolis, Brazil, with a total duration of 8 hours.",
"certificate_logo": "https://example.com/logo.png",
"certificate_background": "https://example.com/background.png",
"order_date": "2025-01-10 14:35:00",
"checkin_latitude": "-27.5667",
"checkin_longitude": "-48.5156",
"time_checkin": "2025-01-15 09:05:00"
}
]
Expand All @@ -105,18 +95,13 @@ Cria múltiplos certificados a partir de uma lista de dados recebida diretamente
- `first_name`: Primeiro nome do participante (string)
- `last_name`: Sobrenome do participante (string)
- `email`: Email do participante (string)
- `phone`: Telefone do participante (string)
- `cpf`: CPF do participante (string, pode ser vazio)
- `city`: Cidade do participante (string)
- `product_id`: ID do produto (integer)
- `product_name`: Nome do produto (string)
- `certificate_details`: Detalhes do certificado (string)
- `certificate_logo`: URL do logo do certificado (string)
- `certificate_background`: URL do background do certificado (string)
- `order_date`: Data do pedido no formato "YYYY-MM-DD HH:MM:SS" (string)
- **Campos opcionais:**
- `checkin_latitude`: Latitude do check-in (string, opcional)
- `checkin_longitude`: Longitude do check-in (string, opcional)
- `time_checkin`: Data e hora do check-in no formato "YYYY-MM-DD HH:MM:SS" (string, opcional)
- **Nota:** Certificados sem `time_checkin` serão marcados como inválidos e não serão processados.
- **Saída (sucesso):**
Expand Down Expand Up @@ -155,7 +140,6 @@ Consulta certificados com base em diferentes critérios.
"product_id": "integer",
"participant_name": "string",
"participant_email": "string",
"participant_document": "string",
"certificate_url": "string",
"created_at": "string",
"updated_at": "string",
Expand Down Expand Up @@ -196,7 +180,6 @@ Lista os certificados de um usuário a partir do e-mail informado no path.
"product_id": 456,
"participant_name": "string",
"participant_email": "string",
"participant_document": "string",
"certificate_url": "string",
"created_at": "2025-01-20T10:30:45",
"updated_at": "2025-01-20T10:30:45",
Expand Down
211 changes: 211 additions & 0 deletions scripts/scrub_pii.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
"""
Limpeza (scrub) de PII já armazenada em itens existentes do DynamoDB.

Remove os atributos de PII que não são mais usados para gerar, entregar ou
validar um certificado. Os nomes dos atributos diferem por tabela:

- participants: cpf, phone, city
- orders: participant_cpf, participant_phone, participant_city,
checkin_latitude, checkin_longitude
- certificates: participant_cpf, participant_phone, participant_city

Segurança:
- Roda em DRY-RUN por padrão (apenas mostra o que faria, não escreve nada).
- Para aplicar de verdade é preciso passar --apply E o nome explícito de cada
tabela. NÃO existe default apontando para produção.
- Deve ser executado DEPOIS de remover os índices cpf/city (ver
py-certify-infra), pois um GSI exige que o atributo-chave exista nos itens.

Uso:
# dry-run (padrão) — só participants
python scripts/scrub_pii.py --participants-table <nome>

# aplicar de verdade nas três tabelas
python scripts/scrub_pii.py \
--participants-table <nome> \
--orders-table <nome> \
--certificates-table <nome> \
--apply
"""

from __future__ import annotations

import argparse
import logging
import os
import sys
from typing import Optional

logger = logging.getLogger("scrub_pii")

# Atributos de PII a remover, por entidade/tabela.
ATTRS_POR_ENTIDADE: dict[str, list[str]] = {
"participants": ["cpf", "phone", "city"],
"orders": [
"participant_cpf",
"participant_phone",
"participant_city",
"checkin_latitude",
"checkin_longitude",
],
"certificates": ["participant_cpf", "participant_phone", "participant_city"],
}


def montar_remocao(
item: dict, atributos_alvo: list[str]
) -> Optional[tuple[str, dict[str, str]]]:
"""
Monta a UpdateExpression de REMOVE apenas para os atributos-alvo que
realmente existem no item. Usa ExpressionAttributeNames para evitar
conflito com palavras reservadas do DynamoDB.

Retorna (update_expression, expression_attribute_names) ou None se o item
não tiver nenhum dos atributos-alvo.
"""
presentes = [attr for attr in atributos_alvo if attr in item]
if not presentes:
return None

nomes = {f"#a{i}": attr for i, attr in enumerate(presentes)}
update_expression = "REMOVE " + ", ".join(nomes.keys())
return update_expression, nomes


def _chaves_da_tabela(client, table_name: str) -> list[str]:
"""Retorna os nomes das colunas-chave (hash/range) da tabela."""
descricao = client.describe_table(TableName=table_name)
return [k["AttributeName"] for k in descricao["Table"]["KeySchema"]]


def _projecao(chaves: list[str], atributos_alvo: list[str]) -> tuple[str, dict[str, str]]:
"""
Monta uma ProjectionExpression que busca apenas as chaves + os atributos-alvo
(evita puxar o item inteiro, minimizando o manuseio de PII).
"""
campos = list(dict.fromkeys([*chaves, *atributos_alvo]))
nomes = {f"#p{i}": campo for i, campo in enumerate(campos)}
return ", ".join(nomes.keys()), nomes


def limpar_tabela(
client, table_name: str, entidade: str, aplicar: bool, limite: Optional[int]
) -> tuple[int, int]:
"""
Varre a tabela e remove os atributos de PII dos itens que os possuem.

Retorna (itens_varridos, itens_afetados).
"""
atributos_alvo = ATTRS_POR_ENTIDADE[entidade]
chaves = _chaves_da_tabela(client, table_name)
proj_expr, proj_nomes = _projecao(chaves, atributos_alvo)

varridos = 0
afetados = 0
paginator = client.get_paginator("scan")
for pagina in paginator.paginate(
TableName=table_name,
ProjectionExpression=proj_expr,
ExpressionAttributeNames=proj_nomes,
):
for item_dynamo in pagina.get("Items", []):
varridos += 1
# item_dynamo vem no formato low-level ({"attr": {"S": "..."}}). Para
# decidir a presença basta olhar as chaves do dict.
remocao = montar_remocao(item_dynamo, atributos_alvo)
if remocao is None:
continue

afetados += 1
update_expression, nomes = remocao
chave = {k: item_dynamo[k] for k in chaves}
removidos = list(nomes.values())

if aplicar:
client.update_item(
TableName=table_name,
Key=chave,
UpdateExpression=update_expression,
ExpressionAttributeNames=nomes,
)
logger.info("[%s] removido %s de %s", table_name, removidos, chave)
else:
logger.info(
"[dry-run][%s] removeria %s de %s", table_name, removidos, chave
)

if limite is not None and varridos >= limite:
return varridos, afetados

return varridos, afetados


def main(argv: Optional[list[str]] = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--participants-table")
parser.add_argument("--orders-table")
parser.add_argument("--certificates-table")
parser.add_argument("--region", default=os.environ.get("AWS_REGION", "us-east-1"))
parser.add_argument(
"--apply",
action="store_true",
help="Aplica de verdade. Sem esta flag roda em dry-run.",
)
parser.add_argument(
"--limit", type=int, default=None, help="Para após N itens (para testes)."
)
args = parser.parse_args(argv)

logging.basicConfig(level=logging.INFO, format="%(message)s")

alvos = {
"participants": args.participants_table,
"orders": args.orders_table,
"certificates": args.certificates_table,
}
alvos = {entidade: nome for entidade, nome in alvos.items() if nome}

if not alvos:
parser.error(
"informe ao menos uma tabela (--participants-table / --orders-table "
"/ --certificates-table). Não há default apontando para produção."
)

if args.apply:
logger.warning(
"*** MODO --apply: alterações serão gravadas em %s ***", list(alvos.values())
)
else:
logger.info("Modo DRY-RUN (nada será gravado). Use --apply para efetivar.")

import boto3 # import tardio: mantém as funções puras testáveis sem boto3

client = boto3.client("dynamodb", region_name=args.region)

total_varridos = 0
total_afetados = 0
for entidade, table_name in alvos.items():
varridos, afetados = limpar_tabela(
client, table_name, entidade, args.apply, args.limit
)
logger.info(
"%s: %d itens varridos, %d itens %s",
table_name,
varridos,
afetados,
"atualizados" if args.apply else "seriam atualizados",
)
total_varridos += varridos
total_afetados += afetados

logger.info(
"TOTAL: %d varridos, %d %s",
total_varridos,
total_afetados,
"atualizados" if args.apply else "seriam atualizados",
)
return 0


if __name__ == "__main__":
sys.exit(main())
1 change: 0 additions & 1 deletion src/application/dto/fetch_certificate_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class FetchCertificateResponseDto(BaseModel):
product_id: Optional[int] = None
participant_name: Optional[str] = None
participant_email: Optional[str] = None
participant_document: Optional[str] = None
certificate_url: Optional[str] = None
certificate_key: Optional[str] = None
created_at: Optional[str] = None
Expand Down
1 change: 0 additions & 1 deletion src/application/dto/list_user_certificates_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class UserCertificateItemDto(BaseModel):
product_id: Optional[int] = None
participant_name: Optional[str] = None
participant_email: Optional[str] = None
participant_document: Optional[str] = None
certificate_url: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
Expand Down
1 change: 0 additions & 1 deletion src/application/list_user_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ def _to_item_dto(self, certificate: Certificate) -> UserCertificateItemDto:
product_id=certificate.product_id,
participant_name=f"{certificate.participant_first_name or ''} {certificate.participant_last_name or ''}".strip(),
participant_email=certificate.participant_email,
participant_document=certificate.participant_cpf,
certificate_url=certificate.certificate_url,
created_at=certificate.generated_date,
updated_at=certificate.generated_date,
Expand Down
18 changes: 2 additions & 16 deletions src/application/mapper/tech_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,11 @@ def to_entity(tech_order_response: TechOrdersResponse) -> Order:
participant_first_name=tech_order_response.first_name,
participant_last_name=tech_order_response.last_name,
participant_email=tech_order_response.email,
participant_phone=tech_order_response.phone,
participant_cpf=tech_order_response.cpf,
participant_city=tech_order_response.city,
product_id=tech_order_response.product_id,
product_name=tech_order_response.product_name,
certificate_details=tech_order_response.certificate_details,
certificate_logo=tech_order_response.certificate_logo,
certificate_background=tech_order_response.certificate_background,
checkin_latitude=tech_order_response.checkin_latitude,
checkin_longitude=tech_order_response.checkin_longitude,
time_checkin=tech_order_response.time_checkin
)

Expand All @@ -35,9 +30,6 @@ def to_entity(tech_product_response: TechOrdersResponse) -> Product:
certificate_details=tech_product_response.certificate_details,
certificate_logo=tech_product_response.certificate_logo,
certificate_background=tech_product_response.certificate_background,
checkin_latitude=tech_product_response.checkin_latitude,
checkin_longitude=tech_product_response.checkin_longitude,
time_checkin=tech_product_response.time_checkin
)

class TechParticipantMapper:
Expand All @@ -46,10 +38,7 @@ def to_entity(tech_order_response: TechOrdersResponse) -> Order:
return Participant(
first_name=tech_order_response.first_name,
last_name=tech_order_response.last_name,
email=tech_order_response.email,
phone=tech_order_response.phone,
cpf=tech_order_response.cpf,
city=tech_order_response.city
email=tech_order_response.email
)

class CertificateMapper:
Expand All @@ -65,8 +54,5 @@ def to_entity(tech_order_response: TechOrdersResponse) -> Certificate:
certificate_background=tech_order_response.certificate_background,
participant_email=tech_order_response.email,
participant_first_name=tech_order_response.first_name,
participant_last_name=tech_order_response.last_name,
participant_cpf=tech_order_response.cpf,
participant_phone=tech_order_response.phone,
participant_city=tech_order_response.city
participant_last_name=tech_order_response.last_name
)
1 change: 0 additions & 1 deletion src/application/strategy/fetch_certificate_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def execute(self, request: FetchCertificateRequestDto) -> List[FetchCertificateR
product_id=certificate.product_id,
participant_name=f"{certificate.participant_first_name or ''} {certificate.participant_last_name or ''}".strip(),
participant_email=certificate.participant_email,
participant_document=certificate.participant_cpf,
certificate_url=certificate.certificate_url,
certificate_key=certificate.certificate_key,
created_at=certificate.generated_date,
Expand Down
3 changes: 0 additions & 3 deletions src/domain/entity/certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,3 @@ class Certificate(BaseModel):
participant_email: Optional[str]
participant_first_name: Optional[str]
participant_last_name: Optional[str]
participant_cpf: Optional[str]
participant_phone: Optional[str]
participant_city: Optional[str]
5 changes: 0 additions & 5 deletions src/domain/entity/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,8 @@ class Order(BaseModel):
certificate_details: str
certificate_logo: str
certificate_background: str
checkin_latitude: str
checkin_longitude: str
time_checkin: str
participant_email: str
participant_first_name: str
participant_last_name: str
participant_cpf: str
participant_phone: str
participant_city: str

Loading