
Faça uma verificação cruzada das visualizações da sua superfície de ataque e encontre os endpoints que não conseguem corroborar uns com os outros.
Faça a verificação cruzada das visões da sua superfície de ataque e encontre os endpoints que não conseguem corroborar uns aos outros.
Um endpoint deveria ser capaz de responder por si mesmo. Ele está no código, então um contrato deveria descrevê-lo. Ele está no contrato, então algo deveria implementá-lo. Ele recebe tráfego real, então é melhor que exista em algum lugar. Quando uma visão conhece um endpoint e as outras não, essa lacuna é o achado.
alibi executa o OWASP noir, lê seu JSON e compara as visões entre si.
O Noir já lê cinco visões independentes da mesma superfície:
| Visão | Lida de |
|---|
| code | mais de 200 analisadores em 33 linguagens |
| doc | OpenAPI, RAML, WSDL, GraphQL SDL, AsyncAPI, gRPC, Smithy, TypeSpec, OData, OpenRPC |
| traffic | HAR, mitmproxy, Burp, Caido, ZAP, Postman, Insomnia, Bruno, .http |
| gateway | nginx, Apache, Envoy, Kong, Traefik, APISIX, Caddy, Istio, Kubernetes Ingress e Gateway API |
| infra | Terraform, CloudFormation, CDK, Serverless, Vercel, Netlify, Wrangler, Azure Functions, Kamal |
O que ele não faz é compará-las. Esse é todo o trabalho aqui, e não requer nenhuma mudança no noir — o alibi o executa uma vez por visão e junta os resultados.
A parte por visão importa. O Noir deduplica por (method, url) em todos os analisadores, então uma rota Flask e um caminho OpenAPI escritos de forma idêntica colapsam em um único endpoint carregando uma única tecnologia. Isso é correto para uma ferramenta de descoberta — é um endpoint — mas apaga a corroboração que esta ferramenta foi feita para medir, e apaga na pior direção possível: quanto melhor duas visões concordam, mais delas desaparecem. O Casdoor é escaneado como 372 endpoints de código e 9 documentados; escaneie apenas seu diretório swagger/ e a especificação tem 235.
--only-techs restringe o conjunto de detectores, então um scan por visão mantém cada uma inteira. Qual tecnologia responde por qual visão é o views.yml; quais tecnologias existem é o que noir list techs reporta.
O alibi não analisa nenhum formato de API por conta própria. Sua única entrada é o JSON do noir.
Requer o noir 1.0.0 ou mais recente no
PATH -- essa é a versão em que noir list techs se tornou um subcomando, e
esse catálogo é o que atribui cada tecnologia a uma visão. O desenvolvimento acompanha a
versão atual do noir. Um binário mais antigo é recusado pelo nome em vez de ser deixado para
falhar na sua primeira leitura de catálogo.
$ uv tool install noir-alibi # or: pipx install noir-alibi
$ alibi scan ./my-service
$ alibi scan # the working directory
$ alibi scan ./service ./contracts ./prod.har # or wherever the views live
Cada caminho é uma fonte, escaneada uma vez por visão. Aponte para o que você tiver — uma árvore de código, um diretório de especificação, um único arquivo de captura — e as visões que estão faltando desativam suas regras em vez de inundar o relatório.
alibi · 1 source · 377 endpoints
code 372 doc 235
230 corroborated -- vouched for by more than one view
19 endpoints nearly matched another view -- these may be matching failures, not real gaps
SHADOW Shadow API -- Implemented, but no contract describes it
134 findings · 4 critical, 57 high, 62 medium, 11 low
critical POST /api/upload-groups router.go:87
upload paths carry more consequence than reads
critical POST /api/upload-permissions router.go:208
...
... and 122 more (SHADOW in full: -f json)
TWO SURFACES?
The doc view is 97% under /api, and 37 of these findings are outside it.
If that is a separate surface the contract never covered, narrow the scan:
alibi scan <paths> --ignore '^/(?!api(/|$))'
If it is the same surface left undocumented, they are the findings that matter most.
Os grupos param em doze — a ordenação é do pior para o melhor, então a cauda é a parte menos
informativa, e -f json tem tudo.
- run: alibi scan . ./contracts -f sarif > alibi.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: alibi.sarif }
O relatório diz que as visões discordam; --endpoints diz o que cada uma delas
continha.
$ alibi scan ./repo -f json --endpoints
Cada visão recebe uma lista: a chave, quais visões a corroboraram, as tecnologias por trás dela, os arquivos, e a grafia antes da normalização — que é onde a diferença sempre está quando duas linhas deveriam ter correspondido e não corresponderam. É três a quatro vezes o resto do payload, então é uma flag em vez do padrão.
Ou faça o gate diretamente: alibi scan . ./contracts --fail-on high sai com código diferente de zero quando
um achado atinge essa severidade. Um scan que o noir não conseguiu ler por completo reporta
executionSuccessful: false, então uma execução degradada não passa como uma limpa.
O Noir mantém a sintaxe de rota de cada framework em vez de inventar uma comum, então o mesmo endpoint chega escrito de várias formas:
python_flask /api/users/<int:user_id>
aiohttp /users/{id}
java_spring /api/catalog/{id}
oas3 /v1/pets/{petId}
rails /posts/:id
nginx /admin/.*
A regra que torna esses comparáveis: o nome de um parâmetro de caminho não faz parte da
sua identidade. {petId} e <int:user_id> descrevem o mesmo slot; só importam
sua posição e se ele abrange um /. Os nomes são mantidos como evidência e
reportados, mas nunca chegam à chave.
Os achados dizem como a correspondência foi feita:
| Grau | Significado |
|---|---|
G1 | as grafias já concordavam |
G2 | concordam depois que a sintaxe de parâmetro é normalizada |
G0 | apenas uma visão o tem — nada foi correspondido |
Uma ferramenta como esta morre por reportar centenas de achados na primeira execução, ou por reportar progresso que ninguém fez. Seis coisas fazem frente:
As regras não disparam sem ambas as visões. Escaneie um código sem contratos em lugar nenhum e todo endpoint tecnicamente se qualifica como uma shadow API não documentada. Esses achados não dizem nada além de que você não forneceu nenhuma documentação, então uma regra só é executada quando toda visão sobre a qual ela raciocina estava de fato no scan. O relatório nomeia as regras que ficaram de fora.
Quase-acertos são reportados como dúvida, não como achados. "No código, não na documentação" é indistinguível de "em ambos, mas o alibi falhou em alinhá-los." Então um endpoint que cai em uma visão é verificado contra as outras em busca de um quase-acerto — mesmo caminho com um verbo diferente, ou um segmento de diferença onde um lado tem um parâmetro e o outro um literal. Achados carregando um quase-acerto são rebaixados e sinalizados para revisão. Essa contagem fica ao lado dos totais, porque cada achado é tão confiável quanto é pequeno.
Visões que nunca se encontraram são um diagnóstico, não centenas de achados. O Argo CD
registra /api em Go e documenta 198 caminhos sob ele, então seu código e sua
especificação não compartilham um único endpoint. Lido literalmente, isso são 58 shadow APIs e
198 contratos fantasma, nenhum deles real. Zero corroboração entre duas
visões populadas significa que a comparação não funcionou — um ponto de montagem representando
as rotas sob ele, ou uma stack que o noir não conseguiu ler — então as regras são
retidas e o motivo é impresso. Caminhos que acabam tendo muitos
endpoints de outras visões sob eles são rotulados como prováveis pontos de montagem.
Quando as duas visões se alinham depois que um prefixo constante sai de uma delas,
o diagnóstico diz isso e nomeia o prefixo. A especificação gerada do Gitea
declara basePath: /GITEA-API-APP-SUBURL/api/v1 enquanto seu roteador Go monta
/api/v1; as visões não compartilham nada, mas 154 dos 535 caminhos documentados correspondem
a um caminho de código depois que esses três segmentos são removidos. Isso é um basePath de spec,
um servers[].url, ou um ponto de montagem que o leitor de código descartou — e é reportado,
nunca aplicado, porque realinhar os caminhos esconderia o bug que ele encontrou.
Uma inundação que na verdade é uma única subárvore ausente é nomeada como uma só: 207 dos
354 contratos fantasma do NodeBB ficam sob /api/v3, onde a visão de código não contém nada
de modo algum.
Uma visão ausente e uma vazia significam coisas opostas. O Noir reporta o que não conseguiu ler, e o alibi imprime isso acima dos achados. O NetBox traz um documento OpenAPI de 12,35MB com 308 caminhos; o noir o pula por exceder seu limite de tamanho de arquivo, e sem esse relatório o alibi afirma que o projeto não documenta nada — não apenas incompleto, mas a resposta errada afirmada com confiança.
Uma regra que parou de ser executada não resolveu nada. Registrar scans e
compará-los reintroduz o mesmo erro à distância: esqueça o
diretório de contratos em uma execução e SHADOW não avalia nada, o que para uma diferença ingênua
parece exatamente como se toda shadow API tivesse sido fechada. No
fixture de cinco visões, descartar um argumento transformou sete achados vigentes em
"resolvidos". Os snapshots registram quais regras avaliaram, as diferenças só consideram
regras que rodaram em ambos os scans, e o resto é nomeado sob NOT COMPARED.
Uma ausência só é evidência quando o sinal existe. Os marcadores de autenticação do Noir cobrem os frameworks que eles conhecem. Em uma stack que eles não cobrem, nada carrega uma tag de autenticação, e tratar isso como "não autenticado" promoveria todo achado e esvaziaria a coluna de severidade de significado. Ajustes que disparam na ausência de uma tag exigem que essa tag apareça em algum lugar do scan primeiro.
| Regra | Condição | Severidade |
|---|---|---|
ORPHAN | recebendo requisições reais, ausente do código | high |
LIVE_UNDOC | recebendo requisições reais, descrito por nenhum contrato | high |
SHADOW | no código, não em nenhum contrato | medium |
DANGLING | uma regra de gateway que não alcança nada implementado | medium |
DRIFT | declarado para implantação, ausente do código | medium |
PHANTOM | em um contrato, não no código | low |
UNEXPOSED | implementado, mas nenhuma regra de gateway o alcança | low |
COLD | implementado, nunca visto recebendo uma requisição | info |
A severidade então muda conforme o que os marcadores do noir encontraram: dados pessoais, uploads de arquivos, nenhum sinal de autenticação, ou um método que altera estado.
Tanto o mapa de visões (views.yml) quanto as regras (rules.yml) são dados, não código.
Um location /api/ representa tudo sob ele, então as regras de gateway e
infraestrutura respondem isto alcança aquele endpoint em vez de isto o contém. Comparados como conjuntos, toda regra de prefixo parece uma rota que ninguém
implementou e toda rota implementada parece inalcançável.
A cobertura é deliberadamente generosa. O Noir reporta o caminho que uma regra corresponde mas não
se ela corresponde como prefixo ou exatamente (location = /x, um Ingress
pathType: Exact), então a exatidão não pode ser recuperada — e tratar toda regra
como prefixo suprime achados em vez de inventá-los.
O relatório diz quanto do código cada visão de roteamento alcança, porque se "34 endpoints que nenhum gateway alcança" é real depende de se essa configuração é a que está na frente do serviço. Nenhum limiar separa isso honestamente: o fixture de teste e2e do Argo CD alcança 39% do seu código e a configuração real do NetBox alcança 100%.
Um catch-all — location /, um Ingress em /, um RewriteRule ^(.*)$ — não é
evidência em nenhum sentido. Ele roteia tudo ou nada, o mesmo para todo
endpoint, então conta como não alcançando nenhum deles. Uma visão de gateway que não contém nada
mais não tem sinal a oferecer, e UNEXPOSED fica de fora e diz isso em vez de
reportar todo endpoint como inalcançável. O Helm chart do Casdoor é exatamente isso:
uma regra de Ingress em /, que lida como evidência produziu 365 achados.
Uma captura HAR registra requisições que aconteceram. Uma coleção Postman registra
requisições que alguém pretendia fazer. ORPHAN, LIVE_UNDOC e COLD todos raciocinam
sobre o que rodou, então exigem uma visão que alguém de fato observou e dizem isso
quando ficam de fora.
O Noir reporta argumentos de CLI, tópicos Kafka e deep links móveis na mesma
lista. cli://gitops-engine/agent achatado em HTTP se torna /agent — colide com qualquer rota web desse nome e é questionado se um gateway
roteia para ele. O protocolo pertence à identidade do endpoint; http e
https são um só espaço e todo o resto mantém o seu próprio.
Algumas lacunas são o estado pretendido. Coloque um .alibi.yml ao lado da fonte:
ignore:
- path: "^/internal/"
why: internal-only admin surface
- rule: UNEXPOSED
path: "^/debug/"
why: not fronted by the gateway in this repo
Ou passe --ignore REGEX para um caso pontual. Achados suprimidos são contados e a
contagem é impressa — uma ferramenta que silenciosamente descarta achados é pior que uma que
imprime demais, porque não há mais como saber o que ela omitiu.
Inicial, mas todas as cinco visões são comparadas.
Medido contra cinco repositórios:
| Repositório | code | doc | corroborado | achados | code↔doc |
|---|---|---|---|---|---|
| casdoor | 372 | 235 | 230 (98%) | 139 | comparado |
| netbox | 1146 | 1193 | 796 (67%) | 746 | comparado |
| argo-cd | 59 | 198 | 1 | 31 | retido |
| authentik | 231 | 1193 | 1 | 192 | retido |
| flipt | 2 | 42 | 0 | 0 | retido |
O Casdoor é o caso mais limpo: 230 dos seus 235 endpoints documentados corresponderam ao código, sem nenhuma falha de normalização de caminho. Todos os 19 quase-acertos eram o mesmo caminho sob um verbo diferente — o noir registrando todo método em um handler catch-all de Go, não um problema de correspondência.
O NetBox é o instrutivo. Ele contém duas superfícies em um repositório: uma UI web renderizada no servidor e uma API REST com DRF-router que só a segunda é documentada. Escaneado por inteiro, ele reporta 746 achados, a maioria deles a observação verdadeira mas inútil de que uma UI web não está em uma especificação de API. Delimitado à superfície que o contrato descreve, ele colapsa para o que de fato valia a pena dizer:
$ alibi scan ./netbox --ignore '^/(?!api(/|$))'
→ 3 shadow APIs: /api/plugins, /api/schema/redoc, /api/schema/swagger-ui,
todas as três genuinamente servidas e genuinamente ausentes do schema. Os 397
fantasmas que permanecem são as operações em massa que a própria subclasse de roteador do NetBox adiciona
a cada endpoint de lista, que nenhuma varredura de urlconf consegue ver.
Os outros três são retidos, cada um por um motivo que vale a pena conhecer:
/api em Go e documenta 198 caminhos sob ele — a
mesma superfície em duas granularidades.urls de cada app
instalado, o que nenhum leitor estático consegue seguir..proto, que é por isso que grpc responde
pela visão de código: arquivado lá, 36 dos seus 36 caminhos documentados corroboram.Que é o teto desta ferramenta, dito claramente: ela compara o que o noir consegue ler, e uma visão lida na granularidade errada é pior que uma não lida de modo algum. A maior parte da maquinaria acima existe para distinguir esses casos em vez de reportá-los como defeitos.
MIT