
Contrasta las vistas de tu superficie de ataque y encuentra los endpoints que no pueden corroborarse entre sí.
Verifica de forma cruzada las vistas de tu superficie de ataque y encuentra los endpoints que no pueden corroborarse entre sí.
Un endpoint debería poder dar cuenta de sí mismo. Está en el código, así que un contrato debería describirlo. Está en el contrato, así que algo debería implementarlo. Recibe tráfico real, así que más vale que exista en algún lugar. Cuando una vista conoce un endpoint y las demás no, esa brecha es el hallazgo.
alibi ejecuta OWASP noir, lee su JSON y compara las vistas entre sí.
Noir ya lee cinco vistas independientes de la misma superficie:
| Vista | Leída desde |
|---|
| code | más de 200 analizadores en 33 lenguajes |
| 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 y Gateway API |
| infra | Terraform, CloudFormation, CDK, Serverless, Vercel, Netlify, Wrangler, Azure Functions, Kamal |
Lo que no hace es compararlas. Ese es todo el trabajo aquí, y no requiere ningún cambio en noir — alibi lo ejecuta una vez por vista y une los resultados.
La parte por vista importa. Noir deduplica por (method, url) entre todos los analizadores, así que una ruta de Flask y una ruta de OpenAPI escritas de forma idéntica colapsan en un solo endpoint que lleva una sola tecnología. Eso es correcto para una herramienta de descubrimiento — es un endpoint — pero borra la corroboración que esta herramienta está construida para medir, y la borra en la peor dirección posible: cuanto mejor coinciden dos vistas, más de ellas desaparecen. Casdoor se escanea como 372 endpoints de código y 9 documentados; escanea solo su directorio swagger/ y la especificación tiene 235.
--only-techs restringe el conjunto de detectores, así que un escaneo por vista mantiene cada una completa. Qué tecnología habla por qué vista es views.yml; qué tecnologías existen es lo que reporte noir list techs.
alibi no analiza ningún formato de API por su cuenta. Su única entrada es el JSON de noir.
Requiere noir 1.0.0 o superior en el
PATH -- esa es la versión en la que noir list techs se convirtió en un subcomando, y ese catálogo es lo que asigna cada tecnología a una vista. El desarrollo sigue la versión actual de noir. Un binario más antiguo es rechazado por su nombre en lugar de dejarlo fallar en su primera lectura del 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 ruta es una fuente, escaneada una vez por vista. Apúntala a lo que tengas — un árbol de código fuente, un directorio de especificaciones, un único archivo de captura — y las vistas que te falten desactivan sus reglas en lugar de inundar el informe.
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.
Los grupos se detienen en doce — el orden es de peor a mejor, así que la cola es la parte menos informativa, y -f json tiene todo.
- run: alibi scan . ./contracts -f sarif > alibi.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: alibi.sarif }
El informe dice que las vistas no coinciden; --endpoints dice qué contenía cada una de ellas.
$ alibi scan ./repo -f json --endpoints
Cada vista obtiene una lista: la clave, qué vistas la corroboraron, las tecnologías detrás de ella, los archivos y la grafía antes de la normalización — que es donde siempre está la diferencia cuando dos filas deberían haber coincidido y no lo hicieron. Es de tres a cuatro veces el resto del payload, así que es una bandera en lugar del valor por defecto.
O filtra directamente: alibi scan . ./contracts --fail-on high sale con código distinto de cero cuando un hallazgo alcanza esa severidad. Un escaneo que noir no pudo leer por completo reporta executionSuccessful: false, así que una ejecución degradada no pasa como una limpia.
Noir conserva la sintaxis de rutas propia de cada framework en lugar de inventar una común, así que el mismo endpoint llega escrito de varias 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/.*
La regla que hace comparables estas formas: el nombre de un parámetro de ruta no es parte de su identidad. {petId} y <int:user_id> describen la misma ranura; solo importan su posición y si abarca un /. Los nombres se conservan como evidencia y se reportan, pero nunca llegan a la clave.
Los hallazgos dicen cómo se hizo la coincidencia:
| Grado | Significado |
|---|---|
G1 | las grafías ya coincidían |
G2 | coinciden una vez normalizada la sintaxis de parámetros |
G0 | solo una vista lo tiene — no se emparejó nada |
Una herramienta como esta muere por reportar cientos de hallazgos en su primera ejecución, o por reportar un progreso que nadie hizo. Seis cosas se oponen:
Las reglas no se disparan sin ambas vistas. Escanea un código base sin contratos en ningún lado y cada endpoint califica técnicamente como una shadow API indocumentada. Esos hallazgos no dicen nada excepto que no proporcionaste ninguna documentación, así que una regla solo se ejecuta cuando cada vista sobre la que razona estaba realmente en el escaneo. El informe nombra las reglas que se quedaron fuera.
Los casi-aciertos se reportan como duda, no como hallazgos. "En el código, no en la documentación" es indistinguible de "en ambos, pero alibi no logró alinearlos". Así que un endpoint que cae en una vista se compara con las demás en busca de un casi-acierto — misma ruta con un verbo diferente, o a un segmento de distancia donde un lado tiene un parámetro y el otro un literal. Los hallazgos que conllevan un casi-acierto se degradan y se marcan para revisión. Ese recuento aparece junto a los totales, porque cada hallazgo es tan fiable como pequeño sea.
Vistas que nunca se encontraron son un diagnóstico, no cientos de hallazgos. Argo CD registra /api en Go y documenta 198 rutas debajo de él, así que su código y su especificación no comparten ni un endpoint. Leído literalmente eso son 58 shadow APIs y 198 contratos fantasma, ninguno real. Cero corroboración entre dos vistas pobladas significa que la comparación no funcionó — un punto de montaje que representa las rutas debajo de él, o un stack que noir no pudo leer — así que las reglas se retienen y se imprime el motivo en su lugar. Las rutas que resultan tener muchos endpoints de otras vistas debajo de ellas se etiquetan como probables puntos de montaje.
Cuando las dos vistas sí se alinean una vez que se quita un prefijo constante de una de ellas, el diagnóstico lo dice y nombra el prefijo. La especificación generada de Gitea declara basePath: /GITEA-API-APP-SUBURL/api/v1 mientras su router de Go monta /api/v1; las vistas no comparten nada, pero 154 de las 535 rutas documentadas coinciden con una ruta de código una vez que se eliminan esos tres segmentos. Eso es un basePath de especificación, un servers[].url, o un punto de montaje que el lector de código omitió — y se reporta, nunca se aplica, porque realinear las rutas ocultaría el bug que encontró.
Una inundación que en realidad es un subárbol faltante se nombra como uno solo: 207 de los 354 contratos fantasma de NodeBB están bajo /api/v3, donde la vista de código no tiene nada en absoluto.
Una vista faltante y una vacía significan cosas opuestas. Noir reporta lo que no pudo leer, y alibi lo imprime por encima de los hallazgos. NetBox incluye un documento OpenAPI de 12,35 MB con 308 rutas; noir lo omite por superar su límite de tamaño de archivo, y sin ese informe alibi afirma que el proyecto no documenta nada — no solo incompleto, sino la respuesta equivocada afirmada con confianza.
Una regla que dejó de ejecutarse no ha resuelto nada. Registrar escaneos y compararlos reintroduce el mismo error a distancia: olvida el directorio de contratos en una ejecución y SHADOW no evalúa nada, lo que para una diferencia ingenua se ve exactamente como si todas las shadow APIs se hubieran cerrado. En el fixture de cinco vistas, eliminar un argumento convirtió siete hallazgos vigentes en "resueltos". Las instantáneas registran qué reglas se evaluaron, las diferencias solo consideran reglas que se ejecutaron en ambos escaneos, y el resto se nombran bajo NOT COMPARED.
Una ausencia solo es evidencia cuando la señal existe. Los etiquetadores de autenticación de noir cubren los frameworks que conocen. En un stack que no cubren, nada lleva una etiqueta de autenticación, y tratar eso como "no autenticado" promovería cada hallazgo y vaciaría de significado la columna de severidad. Los ajustes que se disparan ante una etiqueta ausente requieren que esa etiqueta aparezca primero en algún lugar del escaneo.
| Regla | Condición | Severidad |
|---|---|---|
ORPHAN | recibe solicitudes reales, ausente del código | high |
LIVE_UNDOC | recibe solicitudes reales, no descrito por ningún contrato | high |
SHADOW | en el código, no en ningún contrato | medium |
DANGLING | una regla de gateway que no alcanza nada implementado | medium |
DRIFT | declarado para despliegue, ausente del código | medium |
PHANTOM | en un contrato, no en el código | low |
UNEXPOSED | implementado, pero ninguna regla de gateway lo alcanza | low |
COLD | implementado, nunca visto recibiendo una solicitud | info |
La severidad luego cambia según lo que encontraron los etiquetadores de noir: datos personales, subidas de archivos, ausencia de señales de autenticación, o un método que cambia estado.
Tanto el mapa de vistas (views.yml) como las reglas (rules.yml) son datos, no código.
Un location /api/ representa todo lo que hay debajo, así que las reglas de gateway e infraestructura responden ¿esto alcanza ese endpoint? en lugar de ¿esto lo contiene?. Comparadas como conjuntos, cada regla de prefijo parece una ruta que nadie implementó y cada ruta implementada parece inalcanzable.
La cobertura es deliberadamente generosa. Noir reporta la ruta que una regla coincide pero no si coincide como prefijo o exactamente (location = /x, un Ingress pathType: Exact), así que la exactitud no puede recuperarse — y tratar cada regla como prefijo suprime hallazgos en lugar de inventarlos.
El informe dice cuánto del código alcanza cada vista de enrutamiento, porque si "34 endpoints que ningún gateway alcanza" es real depende de si esa configuración es la que está al frente del servicio. Ningún umbral separa eso honestamente: el fixture de pruebas e2e de Argo CD alcanza el 39% de su código y la configuración real de NetBox alcanza el 100%.
Un catch-all — location /, un Ingress en /, un RewriteRule ^(.*)$ — no es evidencia en ningún sentido. Enruta todo o nada, lo mismo para cada endpoint, así que cuenta como no alcanzar ninguno de ellos. Una vista de gateway que no contiene nada más no tiene señal que ofrecer, y UNEXPOSED se queda fuera y lo dice en lugar de reportar cada endpoint como inalcanzable. El chart de Helm de Casdoor es exactamente eso: una regla de Ingress en /, que leída como evidencia produjo 365 hallazgos.
Una captura HAR registra solicitudes que ocurrieron. Una colección de Postman registra solicitudes que alguien pretendía hacer. ORPHAN, LIVE_UNDOC y COLD razonan sobre lo que se ejecutó, así que requieren una vista que alguien realmente observó y lo dicen cuando se quedan fuera.
Noir reporta argumentos de CLI, temas de Kafka y deep links móviles en la misma lista. cli://gitops-engine/agent aplanado a HTTP se convierte en /agent — colisiona con cualquier ruta web de ese nombre y se le pregunta si un gateway enruta hacia ella. El protocolo pertenece a la identidad del endpoint; http y https son un solo espacio y todo lo demás conserva el suyo.
Algunas brechas son el estado previsto. Coloca un .alibi.yml junto a la fuente:
ignore:
- path: "^/internal/"
why: internal-only admin surface
- rule: UNEXPOSED
path: "^/debug/"
why: not fronted by the gateway in this repo
O pasa --ignore REGEX para una excepción puntual. Los hallazgos suprimidos se cuentan y el recuento se imprime — una herramienta que descarta hallazgos en silencio es peor que una que imprime demasiados, porque ya no hay forma de saber qué ocultó.
Temprano, pero las cinco vistas se comparan.
Medido contra cinco repositorios:
| Repositorio | code | doc | corroborated | findings | code↔doc |
|---|---|---|---|---|---|
| casdoor | 372 | 235 | 230 (98%) | 139 | compared |
| netbox | 1146 | 1193 | 796 (67%) | 746 | compared |
| argo-cd | 59 | 198 | 1 | 31 | held back |
| authentik | 231 | 1193 | 1 | 192 | held back |
| flipt | 2 | 42 | 0 | 0 | held back |
Casdoor es el caso más limpio: 230 de sus 235 endpoints documentados coincidieron con el código, sin ningún fallo de normalización de rutas. Los 19 casi-aciertos eran la misma ruta bajo un verbo diferente — noir registrando cada método en un manejador catch-all de Go, no un problema de emparejamiento.
NetBox es el instructivo. Contiene dos superficies en un solo repositorio: una interfaz web renderizada en servidor y una API REST con DRF-router que solo la segunda está documentada. Escaneado completo reporta 746 hallazgos, la mayoría de ellos la observación verdadera pero inútil de que una interfaz web no está en una especificación de API. Acotado a la superficie que el contrato describe, colapsa a lo que realmente valía la pena decir:
$ alibi scan ./netbox --ignore '^/(?!api(/|$))'
→ 3 shadow APIs: /api/plugins, /api/schema/redoc, /api/schema/swagger-ui,
las tres realmente servidas y realmente ausentes del esquema. Los 397
fantasmas que quedan son las operaciones masivas que la propia subclase de router de NetBox añade a cada endpoint de lista, que ningún recorrido de urlconf puede ver.
Las otras tres se retienen, cada una por un motivo que vale la pena conocer:
/api en Go y documenta 198 rutas debajo de él — la misma superficie en dos granularidades.urls de cada app instalada, que ningún lector estático puede seguir..proto, que es por lo que grpc habla por la vista de código: archivado allí, 36 de sus 36 rutas documentadas corroboran.Que es el techo de esta herramienta, dicho claramente: compara lo que noir puede leer, y una vista leída con la granularidad equivocada es peor que una no leída en absoluto. La mayor parte de la maquinaria anterior existe para distinguir esos casos en lugar de reportarlos como defectos.
MIT