Recoupez les vues de votre surface d'attaque et identifiez les points de terminaison qui ne peuvent pas se corroborer mutuellement.
Recoupez les vues de votre surface d'attaque et trouvez les endpoints qui ne peuvent pas se corroborer mutuellement.
Un endpoint devrait pouvoir rendre compte de lui-même. Il est dans le code, donc un contrat devrait le décrire. Il est dans le contrat, donc quelque chose devrait l'implémenter. Il reçoit du trafic réel, donc il ferait mieux d'exister quelque part. Quand une vue connaît un endpoint et que les autres non, cet écart est le constat.
alibi exécute OWASP noir, lit son JSON et compare les vues entre elles.
Noir lit déjà cinq vues indépendantes de la même surface :
| Vue | Lue depuis |
|---|
| code | plus de 200 analyseurs couvrant 33 langages |
| 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 et Gateway API |
| infra | Terraform, CloudFormation, CDK, Serverless, Vercel, Netlify, Wrangler, Azure Functions, Kamal |
Ce qu'il ne fait pas, c'est les comparer. C'est tout le travail ici, et cela ne nécessite aucune modification de noir — alibi l'exécute une fois par vue et joint les résultats.
La partie par vue est importante. Noir déduplique par (method, url) à travers tous les analyseurs, donc une route Flask et un chemin OpenAPI orthographiés identiquement fusionnent en un seul endpoint portant une seule technologie. C'est correct pour un outil de découverte — c'est un seul endpoint — mais cela efface la corroboration que cet outil est conçu pour mesurer, et l'efface dans la pire direction possible : mieux deux vues s'accordent, plus elles disparaissent. Casdoor se scanne comme 372 endpoints de code et 9 documentés ; scannez son répertoire swagger/ seul et la spécification en compte 235.
--only-techs restreint le pool de détecteurs, donc un scan par vue garde chacune entière. Quelle technologie parle pour quelle vue, c'est views.yml ; quelles technologies existent, c'est ce que rapporte noir list techs.
alibi ne parse aucun format d'API par lui-même. Sa seule entrée est le JSON de noir.
Nécessite noir 1.0.0 ou plus récent dans le
PATH — c'est la version où noir list techs est devenu une sous-commande, et
ce catalogue est ce qui assigne chaque technologie à une vue. Le développement suit la
version actuelle de noir. Un binaire plus ancien est refusé par son nom plutôt que laissé
échouer à sa première lecture de catalogue.
$ 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
Chaque chemin est une source, scannée une fois par vue. Pointez-le vers ce que vous avez — une arborescence de sources, un répertoire de spécifications, un seul fichier de capture — et les vues qui vous manquent désactivent leurs règles plutôt que d'inonder le rapport.
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.
Les groupes s'arrêtent à douze — l'ordre va du pire au meilleur, donc la queue est la partie la moins
informative, et -f json contient tout.
- run: alibi scan . ./contracts -f sarif > alibi.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: alibi.sarif }
Le rapport dit que les vues divergent ; --endpoints dit ce que chacune d'elles
contenait.
$ alibi scan ./repo -f json --endpoints
Chaque vue obtient une liste : la clé, quelles vues l'ont corroborée, les technologies derrière elle, les fichiers, et l'orthographe avant normalisation — ce qui est là où la différence se trouve toujours quand deux lignes auraient dû correspondre et ne l'ont pas fait. C'est trois à quatre fois le reste de la charge utile, donc c'est un flag plutôt que le comportement par défaut.
Ou filtrez directement : alibi scan . ./contracts --fail-on high sort avec un code non nul quand
un constat atteint cette sévérité. Un scan que noir n'a pas pu lire entièrement rapporte
executionSuccessful: false, donc une exécution dégradée ne passe pas pour une exécution propre.
Noir conserve la syntaxe de route propre à chaque framework plutôt que d'en inventer une commune, donc le même endpoint arrive orthographié de plusieurs façons :
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 règle qui les rend comparables : le nom d'un paramètre de chemin ne fait pas partie de
son identité. {petId} et <int:user_id> décrivent le même emplacement ; seuls sa
position et le fait qu'il couvre ou non un / importent. Les noms sont conservés comme preuve et
rapportés, mais n'atteignent jamais la clé.
Les constats indiquent comment la correspondance a été faite :
| Grade | Signification |
|---|---|
G1 | les orthographes concordaient déjà |
G2 | elles concordent une fois la syntaxe des paramètres normalisée |
G0 | une seule vue le possède — rien n'a été mis en correspondance |
Un outil comme celui-ci meurt en rapportant des centaines de constats à sa première exécution, ou en rapportant des progrès que personne n'a faits. Six choses s'y opposent :
Les règles ne se déclenchent pas sans les deux vues. Scannez une base de code sans aucun contrat nulle part et chaque endpoint se qualifie techniquement comme une shadow API non documentée. Ces constats ne disent rien d'autre que le fait que vous n'avez fourni aucune documentation, donc une règle ne s'exécute que quand chaque vue sur laquelle elle raisonne était réellement dans le scan. Le rapport nomme les règles qui sont restées à l'écart.
Les quasi-correspondances sont rapportées comme un doute, pas comme des constats. « Dans le code, pas dans la doc » est indiscernable de « dans les deux, mais alibi a échoué à les aligner ». Donc un endpoint qui atterrit dans une vue est vérifié contre les autres pour une quasi- correspondance — même chemin avec un verbe différent, ou à un segment près là où un côté a un paramètre et l'autre un littéral. Les constats portant une quasi-correspondance sont rétrogradés et signalés pour revue. Ce compte se trouve à côté des totaux, parce que chaque constat n'est fiable que dans la mesure où il est petit.
Des vues qui ne se sont jamais rencontrées sont un diagnostic, pas des centaines de constats. Argo CD
enregistre /api en Go et documente 198 chemins en dessous, donc son code et sa
spécification ne partagent pas un seul endpoint. Lu littéralement, cela fait 58 shadow APIs et
198 contrats fantômes, aucun d'entre eux réel. Zéro corroboration entre deux
vues peuplées signifie que la comparaison n'a pas fonctionné — un point de montage qui tient lieu
des routes en dessous, ou une stack que noir n'a pas pu lire — donc les règles sont
retenues et la raison est imprimée à la place. Les chemins qui s'avèrent avoir de nombreux
endpoints d'autres vues en dessous sont étiquetés comme points de montage probables.
Quand les deux vues s'alignent une fois qu'un préfixe constant est retiré de l'une d'elles,
le diagnostic le dit et nomme le préfixe. La spécification générée de Gitea
déclare basePath: /GITEA-API-APP-SUBURL/api/v1 tandis que son routeur Go monte
/api/v1 ; les vues ne partagent rien, mais 154 des 535 chemins documentés correspondent
à un chemin de code une fois ces trois segments retirés. C'est un basePath de spec,
un servers[].url, ou un point de montage que le lecteur de code a abandonné — et c'est rapporté,
jamais appliqué, parce que réaligner les chemins masquerait le bug qu'il a trouvé.
Un flot qui est en réalité un seul sous-arbre manquant est nommé comme tel : 207 des 354 contrats
fantômes de NodeBB se trouvent sous /api/v3, où la vue code ne contient rien
du tout.
Une vue manquante et une vue vide signifient des choses opposées. Noir rapporte ce qu'il n'a pas pu lire, et alibi l'imprime au-dessus des constats. NetBox livre un document OpenAPI de 12,35 Mo avec 308 chemins ; noir l'ignore pour dépassement de sa limite de taille de fichier, et sans ce rapport alibi déclare que le projet ne documente rien — pas simplement incomplet, mais la mauvaise réponse énoncée avec assurance.
Une règle qui a cessé de s'exécuter n'a rien résolu. Enregistrer des scans et
les comparer réintroduit la même erreur à distance : oubliez le
répertoire de contrats lors d'une exécution et SHADOW n'évalue rien, ce qui pour une différence naïve
ressemble exactement à la fermeture de toutes les shadow APIs. Sur
le fixture à cinq vues, retirer un argument a transformé sept constats persistants en
« résolus ». Les instantanés enregistrent quelles règles ont été évaluées, les différences ne considèrent que
les règles qui se sont exécutées dans les deux scans, et le reste est nommé sous NOT COMPARED.
Une absence n'est une preuve que quand le signal existe. Les tagueurs d'auth de Noir couvrent les frameworks qu'ils connaissent. Dans une stack qu'ils ne couvrent pas, rien ne porte de tag d'auth, et traiter cela comme « non authentifié » promeut chaque constat et vide la colonne de sévérité de son sens. Les ajustements qui se déclenchent sur un tag manquant exigent que ce tag apparaisse quelque part dans le scan au préalable.
| Règle | Condition | Sévérité |
|---|---|---|
ORPHAN | reçoit de vraies requêtes, absent du code | high |
LIVE_UNDOC | reçoit de vraies requêtes, décrit par aucun contrat | high |
SHADOW | dans le code, dans aucun contrat | medium |
DANGLING | une règle de gateway qui n'atteint rien d'implémenté | medium |
DRIFT | déclaré pour le déploiement, absent du code | medium |
PHANTOM | dans un contrat, pas dans le code | low |
UNEXPOSED | implémenté, mais aucune règle de gateway ne l'atteint | low |
COLD | implémenté, jamais vu recevant une requête | info |
La sévérité évolue ensuite selon ce que les tagueurs de noir ont trouvé : données personnelles, uploads de fichiers, aucun signe d'authentification, ou une méthode qui change l'état.
La carte des vues (views.yml) et les règles (rules.yml) sont des données, pas du code.
Un seul location /api/ représente tout ce qui se trouve en dessous, donc les règles de gateway et
d'infrastructure répondent à est-ce que ceci atteint cet endpoint plutôt qu'à est-ce que ceci le contient. Comparées
comme des ensembles, chaque règle de préfixe ressemble à une route que personne n'a implémentée et chaque route implémentée
semble inatteignable.
La couverture est délibérément généreuse. Noir rapporte le chemin qu'une règle correspond mais pas
si elle correspond comme préfixe ou exactement (location = /x, un Ingress
pathType: Exact), donc l'exactitude ne peut pas être récupérée — et traiter chaque règle
comme un préfixe supprime des constats plutôt que d'en inventer.
Le rapport indique quelle proportion du code chaque vue de routage atteint, parce que savoir si « 34 endpoints qu'aucun gateway n'atteint » est réel dépend de si cette config est celle qui front le service. Aucun seuil ne sépare cela honnêtement : le fixture de test e2e d'Argo CD atteint 39 % de son code et la vraie config de NetBox atteint 100 %.
Un catch-all — location /, un Ingress à /, un RewriteRule ^(.*)$ — n'est pas
une preuve dans un sens ou dans l'autre. Il route tout ou rien, pareil pour chaque
endpoint, donc il compte comme n'en atteignant aucun. Une vue gateway ne contenant rien
d'autre n'a aucun signal à offrir, et UNEXPOSED reste à l'écart et le dit plutôt que
de rapporter chaque endpoint comme inatteignable. Le chart Helm de Casdoor est exactement cela :
une règle Ingress à /, qui lue comme preuve a produit 365 constats.
Une capture HAR enregistre des requêtes qui ont eu lieu. Une collection Postman enregistre
des requêtes que quelqu'un avait l'intention de faire. ORPHAN, LIVE_UNDOC et COLD raisonnent tous
sur ce qui s'est exécuté, donc ils exigent une vue que quelqu'un a réellement observée et le disent
quand ils restent à l'écart.
Noir rapporte les arguments CLI, les topics Kafka et les deep links mobiles dans la même
liste. cli://gitops-engine/agent aplati en HTTP devient /agent — il
entre en collision avec toute route web de ce nom et on lui demande si un gateway
route vers lui. Le protocole fait partie de l'identité de l'endpoint ; http et
https sont un seul espace et tout le reste garde le sien.
Certains écarts sont l'état voulu. Placez un .alibi.yml à côté de la source :
ignore:
- path: "^/internal/"
why: internal-only admin surface
- rule: UNEXPOSED
path: "^/debug/"
why: not fronted by the gateway in this repo
Ou passez --ignore REGEX pour un cas ponctuel. Les constats supprimés sont comptés et le
compte est imprimé — un outil qui abandonne discrètement des constats est pire qu'un qui en
imprime trop, parce qu'il n'y a plus aucun moyen de savoir ce qu'il a retenu.
Précoce, mais les cinq vues sont comparées.
Mesuré sur cinq dépôts :
| Dépôt | code | doc | corroboré | constats | code↔doc |
|---|---|---|---|---|---|
| casdoor | 372 | 235 | 230 (98%) | 139 | comparé |
| netbox | 1146 | 1193 | 796 (67%) | 746 | comparé |
| argo-cd | 59 | 198 | 1 | 31 | retenu |
| authentik | 231 | 1193 | 1 | 192 | retenu |
| flipt | 2 | 42 | 0 | 0 | retenu |
Casdoor est le cas le plus propre : 230 de ses 235 endpoints documentés correspondaient au code, sans aucun échec de normalisation de chemin. Les 19 quasi-correspondances étaient le même chemin sous un verbe différent — noir enregistrant chaque méthode sur un handler catch-all Go, pas un problème de correspondance.
NetBox est le cas instructif. Il contient deux surfaces dans un seul dépôt : une interface web rendue côté serveur et une API REST DRF-router que seule la seconde est documentée. Scanné en entier, il rapporte 746 constats, la plupart étant l'observation vraie mais inutile qu'une interface web n'est pas dans une spécification d'API. Restreint à la surface que le contrat décrit, il se réduit à ce qui valait réellement la peine d'être dit :
$ alibi scan ./netbox --ignore '^/(?!api(/|$))'
→ 3 shadow APIs : /api/plugins, /api/schema/redoc, /api/schema/swagger-ui,
toutes les trois réellement servies et réellement absentes du schéma. Les 397
fantômes qui restent sont les opérations en masse que la propre sous-classe de routeur de NetBox ajoute
à chaque endpoint de liste, que nul parcours d'urlconf ne peut voir.
Les trois autres sont retenus, chacun pour une raison qui vaut la peine d'être connue :
/api en Go et documente 198 chemins en dessous — la
même surface à deux granularités.urls de chaque application
installée, ce qu'aucun lecteur statique ne peut suivre..proto, c'est pourquoi grpc parle
pour la vue code : classé là, 36 de ses 36 chemins documentés se corroborent.Ce qui est le plafond de cet outil, dit clairement : il compare ce que noir peut lire, et une vue lue à la mauvaise granularité est pire qu'une vue pas lue du tout. La plupart de la machinerie ci-dessus existe pour distinguer ces cas plutôt que pour les rapporter comme des défauts.
MIT