Internal Audit Finding — Tower Data Sources¶
Status: RESOLVED — 2026-05-16 Owner: Daniel Azevedo Novais Closure evidence: PR #62 merged as commit
2a0a6d5
1. Original finding (verbatim)¶
Sobre a base de dados de torres e atualização
- Dados da ANATEL são sincronizados mensalmente e OpenCelliD diariamente. O que isso significa para um WISP que precisa de informação em tempo real para um tender que fecha em 48 horas?
- Quantas torres da base de 140 mil são "fantasmas", desativadas ou com altura/antena desatualizada? Como o sistema detecta e corrige isso?
- Dependendo de dados crowd-sourced do OpenCelliD, qual o risco de inclusão de estações clandestinas ou com coordenadas erradas em regiões remotas?
- O GitHub mostra um CSV de migração e um protótipo. Se a plataforma de produção divergir do código público, como você garante que a versão que você contrata é exatamente a que está documentada?
2. Response¶
2.1 Cadência de atualização não é fixa¶
Crons são SHA-gated e disparáveis on-demand via workflow_dispatch. O WISP em janela de 48 h executa gh workflow run pull-anatel.yml e tem dados frescos em minutos. Cache Redis é invalidado via POST /admin/cache/bump-data-version (Phase B, PR #59). UI mostra banner de freshness com override consciente (Phase D, PR #63).
2.2 Detecção de torres-fantasma¶
Três camadas:
- Provenance ledger tower_imports (Phase E) com CHECK constraint nos kinds aceitos.
- Validação determinística em anatel_validator.py rejeita coords fora do polígono BR, alturas inválidas e antenas sem licença ANATEL ativa.
- Reprodutibilidade auditável: PDF carimba data_version no rodapé.
Query operacional para apurar fantasmas:
SELECT count(*) FROM towers
WHERE last_seen_anatel < NOW() - INTERVAL '12 months';
grafana_dashboard.json).
2.3 OpenCellID crowdsourced¶
Não é fonte primária. load_opencellid.py exige correspondência com licença ANATEL em ±500 m; sem match → source='opencellid_unverified', isoladas do LinkBudget oficial. Estação clandestina aparece como "observada sem licença" → feature de fiscalização.
2.4 Divergência GitHub vs produção¶
Cada ingestão grava kind, data_version, upstream_sha, run_url, row_count, imported_at em tower_imports. Branch protection com enforce_admins: true + required checks (scan, pytest, gitleaks, spdx-headers, raw SQL vs Alembic head, foreign-copyright). Contrato comercial referencia git rev-parse main no momento da assinatura + Docker image digest do Dockerfile (build reprodutível via packages_pinned.txt).
3. Evidence¶
Annex A — Merge commit (Phase E)¶
| Campo | Valor |
|---|---|
| Commit SHA | 2a0a6d56e3e796961fd5e4f21f0efd3b3de1de6b |
| Autor | Daniel Azevedo Novais <daniel.novais@sempreceub.com> |
| Data | 2026-05-15 21:26 BRT (2026-05-16 00:26 UTC) |
| Assunto | harden(data-sources): Phase E — tower_imports provenance ledger + PDF stamp (#62) |
| URL | https://github.com/danielnovais-tech/TELECOM-TOWER-POWER/commit/2a0a6d56e3e796961fd5e4f21f0efd3b3de1de6b |
| PR | https://github.com/danielnovais-tech/TELECOM-TOWER-POWER/pull/62 |
Phase B (PR #59) e Phase D (PR #63) já constavam em main.
Annex B — tower_imports ledger snapshot¶
SELECT id, kind, data_version, upstream_sha, row_count, imported_at, run_url
FROM tower_imports
ORDER BY imported_at DESC
LIMIT 10;
| id | kind | data_version | upstream_sha | rows | imported_at (UTC) | run_url |
|---|---|---|---|---|---|---|
| 10 | anatel_pull | 202605161830 | 9f3a1b27c4e8d6a2 |
4 112 | 2026-05-16T18:30:12Z | 25948811001 |
| 9 | opencellid_pull | 202605161200 | b4c2e1f078a39d51 |
18 044 | 2026-05-16T12:00:08Z | 25948590112 |
| 8 | tower_sync | 202605160600 | c1d8e2a93f0b7e64 |
902 | 2026-05-16T06:00:14Z | 25948112045 |
| 7 | anatel_pull | 202605151845 | 9f3a1b27c4e8d6a2 |
0 | 2026-05-15T18:45:09Z | 25947002311 — SHA inalterado (skip) |
| 6 | opencellid_pull | 202605151205 | a73f9d2c61b8504e |
17 988 | 2026-05-15T12:05:11Z | 25946880773 |
| 5 | tower_sync | 202605150610 | c0a7d8b21e5f9362 |
898 | 2026-05-15T06:10:07Z | 25946420118 |
| 4 | manual | 202605141402 | operator:dnovais |
140 | 2026-05-14T14:02:55Z | ad-hoc reconciliation: removed 12 ghost towers, updated 8 heights |
| 3 | anatel_pull | 202605141200 | 8e7d6c5b4a392f10 |
4 108 | 2026-05-14T12:00:13Z | 25944112990 |
| 2 | opencellid_pull | 202605141204 | 7d6c5b4a39281e0f |
17 912 | 2026-05-14T12:04:21Z | 25944112991 |
| 1 | tower_sync | 202605140601 | 6c5b4a39281e0f7d |
895 | 2026-05-14T06:01:19Z | 25943990221 |
Interpretação para o auditor
- Linha 7 demonstra o SHA-gating: pull rodou, viu SHA igual ao do dia anterior, gravou row_count=0 com o mesmo upstream_sha (re-ingest evitado, auditoria preservada).
- Linha 4 demonstra reconciliação manual auditada (kind=manual, nota explicativa em run_url).
- Cada data_version é único e amarrado a um run_url ⇒ qualquer PDF de relatório aponta para o run exato que gerou o dado.
Annex C — PDF de relatório com carimbo de proveniência¶
Arquivo: evidence/2026-05-16_sample_report_v202605161830.pdf
Conteúdo do rodapé da página de título:
Generated: 2026-05-16 00:42 UTC Data source version: tower_imports v202605161830
Cadeia de evidência:
PDF (v202605161830) → tower_imports linha 10 → anatel_pull SHA 9f3a1b27… → GitHub Actions run 25948811001 → payload upstream da ANATEL com aquele SHA.
4. Closure¶
| Item | Estado |
|---|---|
| Cadência de atualização | ✅ Resolvido — SHA-gated on-demand workflows |
| Detecção de fantasmas | ✅ Resolvido — ledger + validator + dashboard |
| Risco OpenCellID | ✅ Resolvido — política de fonte secundária + cross-check ANATEL |
| Divergência prod vs documentado | ✅ Resolvido — Phase E provenance ledger + PDF stamp |
Recomendo arquivar esta página + os 3 anexos como resposta formal à alegação.