Libellula

Documentação.

O contrato da API: o formato das chamadas e, principalmente, os seis limites que ela não permite contornar.

No ar

A API responde em https://libellula.dev/api/v1. Não há host separado — api.libellula.dev não resolve, e não vamos publicar um nome só para ele responder erro.

A chave é criada em /app/integracoes/api e aparece uma vez. O que fica no banco é o sha256 dela: um dump vazado entrega a lista de chaves e nenhuma chave. Não há “revelar de novo”, porque poder revelar significaria guardar.

As rotas

Seis endereços, todos de leitura. Cada um exige um escopo, e o índice diz quais a sua chave alcança — para você descobrir isso numa chamada em vez de numa recusa.

Rotas da API v1, com o escopo que cada uma exige
RotaEscopoO que devolve
GET /api/v1—O índice. Devolve as rotas e diz quais a sua chave alcança.
GET /api/v1/ativosassetsAtivos do inventário, com estado e prova de propriedade.
GET /api/v1/achadosfindingsAchados, com estado, severidade e CVE quando houver.
GET /api/v1/prioridadesfindingsA fila de risco, com o score e a razão dele.
GET /api/v1/autorizacoesauthorizationsAutorizações de coleta, com janela e escopo.
GET /api/v1/trilhaauditA trilha de auditoria — o envelope do evento, nunca o conteúdo.

Teto de 120 requisições por minuto, por chave, aplicado pelo banco na mesma escrita que confere a credencial — separar conferir de contar abriria a janela em que um teto deixa de valer sob carga, que é quando ele importa. Estourar devolve 429; credencial ruim devolve 401. A distinção existe porque quem estourou o teto deve tentar de novo, e quem tem credencial ruim não deve.

Formato das chamadas

Cabeçalho de credencial, paginação por cursor, e um envelope de erro só.

Listar achados confirmados

curl https://libellula.dev/api/v1/achados \
  -H "Authorization: Bearer $LIBELLULA_KEY" \
  -G \
  -d estado=confirmed \
  -d limite=50

severidade nula não é omissão: é o estado do produto. Achado suspeito não tem severidade, e o banco recusa que tenha. Quem vir null está vendo a regra, não um buraco.

Paginação por cursor

# A resposta traz o cursor da próxima página em "proximo".
# Nulo quer dizer que acabou.
{
  "itens": [ … ],
  "proximo": "FND-4471"
}

curl https://libellula.dev/api/v1/achados \
  -H "Authorization: Bearer $LIBELLULA_KEY" \
  -G -d limite=50 -d depois=FND-4471

Cursor, e não ?pagina=3. Deslocamento mente quando linhas entram ou saem entre uma página e a seguinte: você pula registros e não tem como saber. Com cursor, o pior caso é repetir um — que é visível.

O envelope de erro

{
  "erro": {
    "codigo": "sem_escopo",
    "mensagem": "Esta chave não tem o escopo \"findings\"."
  }
}

Um formato só, inclusive quando a plataforma quebra. Isto apareceu rodando: uma rota foi ao ar com nome de coluna errado e respondeu 500 sem corpo nenhum — nada vazou, e nada explicou. O detalhe do erro vai para o nosso log; descrever o esquema para quem chama seria dar o mapa a quem tem a chave de uma porta.

Verificar assinatura de webhook

// O motor assina o envelope cru, e nada mais.
const cru = JSON.stringify({
  org, seq, action, target, severity, occurred_at
});

const esperado = "sha256=" + crypto
  .createHmac("sha256", segredo)
  .update(cru)
  .digest("hex");

crypto.timingSafeEqual(
  Buffer.from(esperado),
  Buffer.from(req.headers["x-libellula-signature"])
);

O que sai no webhook é o envelope do evento — organização, sequência, ação, alvo, severidade e instante. Nunca o payload: ele pertence à organização e, em eventos de revelação, descreve acesso a dado pessoal.

A única porta que escreve, e por que ela não é a v1

Ingestão de evento de sensor. Credencial própria, um verbo, e nenhum GET.

Enviar evento reduzido

curl -X POST https://libellula.dev/api/sensor \
  -H "Authorization: Bearer $LIBELLULA_SENSOR" \
  -H "Content-Type: application/json" \
  -d '{ "eventos": [
    { "em": "2026-08-30T14:22:07Z",
      "classe": "admin-inexistente",
      "regra": "waf:sqli-generic",
      "origem": "9f3a1c4e77b20de1" }
  ] }'

# { "gravados": 1, "recusados": [] }

O que ela recebe, e o que recusa

O instante, a classe do caminho, a regra que disparou no seu equipamento e a origem como resumo salgado. Nunca a linha de log, nunca o corpo, nunca o agente, nunca o endereço — a restrição de formato da coluna recusa um IP inclusive vindo de um sensor mal configurado.

Campo desconhecido é recusado com o nome dele. Um campo ignorado em silêncio é um vazamento que ninguém descobre: quem mandou acredita que chegou, e quem recebeu acredita que não veio.

Lote de até 200 eventos por chamada, com resposta parcial — um evento inválido no meio não derruba os outros. Não há GET: uma credencial de sensor que também lesse transformaria “alguém inventa evento” em “alguém exporta o padrão de tráfego da organização”.

O que responde sem credencial

Um endereço, e ele é aberto pelo mesmo motivo que uma chave pública existe.

A chave de verificação de exportação

# A metade pública, servida sem autenticação.
curl https://libellula.dev/api/assinatura

# Confira uma exportação assinada: serialize o campo "conteudo"
# do próprio arquivo, compare o sha256 e verifique a assinatura
# Ed25519 sobre ele. O cabeçalho está dentro do conteúdo assinado.

Por que sem login

Quem confere uma exportação assinada é o auditor externo, o cliente do nosso cliente, o jurídico do outro lado — gente sem conta aqui. Uma assinatura que exigisse login para ser verificada não serviria ao caso de uso que a justifica.

O que sai é material público por definição: os 32 bytes da metade pública de um par Ed25519. Rotacionar a chave mantém a anterior publicada — exportação assinada verifica só com a chave que a assinou, e despublicar a antiga transformaria em papel sem valor tudo o que já saiu.

Limites explícitos

O que a API não permite, documentado com o mesmo cuidado do que ela permite.

A v1 lê. Não escreve.

Nenhuma rota de /api/v1 cria, altera ou apaga — e isso não é etapa de implementação, é a decisão. Escrita por API é onde o dano acontece, e uma chave vazada que só lê é um incidente de confidencialidade, não de integridade. O dia em que houver caso de uso real para escrever, ele entra numa v2 com o cuidado que merece.

Não existe escopo que revele dado pessoal

evidence_pii não é concedível, e a recusa está numa restrição do banco, não numa validação de formulário. Nenhuma rota devolve conteúdo de evidência. A revelação tem cerimônia própria na interface — finalidade declarada, janela de 60 segundos, limite diário e evento de trilha de severidade alta —, e uma chave de API é exatamente o caminho que dissolveria essa cerimônia sem ninguém notar.

Não existe rota de edição ou exclusão na trilha

A trilha é append-only para qualquer papel e qualquer credencial, incluindo o proprietário da organização. Não há rota porque não há função: o privilégio não existe no banco.

A chave não aciona nem reativa o kill-switch

Nenhuma das duas coisas passa por aqui. Acionar e reativar são atos de interface, com nome do alvo digitado e motivo obrigatório — e o nível global exige duas assinaturas de operadores distintos para se sustentar.

A chave não aprova simulação

A dupla aprovação exige duas pessoas distintas, e a chave conta como a pessoa que a criou. Ela não pode ser a segunda de si mesma; a chave primária da tabela de aprovação impede.

A chave age como uma pessoa, e a trilha continua com um nome

Uma credencial que agisse como “sistema” criaria uma segunda categoria de autor na auditoria, e a primeira pergunta de qualquer investigação — “quem criou essa chave?” — passaria a ser respondida em outro lugar. Toda chave carrega a pessoa que a criou.

Documentar apenas o que a API permite deixa a pergunta mais importante sem resposta: o que acontece se alguém tentar contornar o escopo por integração. A resposta é que nada da v1 enfileira coleta — e, no dia em que algo enfileirar, a revalidação continuará no motor, não na camada de API. É por isso que nenhum destes seis limites depende de a chamada ser bem-comportada.