
공격 표면의 뷰를 교차 검증하고 서로를 입증할 수 없는 엔드포인트를 찾아내세요.
공격 표면에 대한 관점들을 교차 검증하고 서로를 입증할 수 없는 엔드포인트를 찾아냅니다.
엔드포인트는 스스로를 설명할 수 있어야 합니다. 코드에 있으니 계약이 그것을 설명해야 합니다. 계약에 있으니 무언가가 그것을 구현해야 합니다. 실제 트래픽을 받으니 어딘가에 존재해야 합니다. 한 관점은 엔드포인트를 알고 있는데 다른 관점들은 모른다면, 그 간극이 바로 발견 사항입니다.
alibi는 OWASP noir를 실행하고, 그 JSON을 읽어, 관점들을 서로 비교합니다.
Noir는 이미 동일한 표면에 대한 다섯 개의 독립적인 관점을 읽습니다:
| 관점 | 읽는 대상 |
|---|---|
| code | 33개 언어에 걸친 200개 이상의 분석기 |
| 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 및 Gateway API |
| infra | Terraform, CloudFormation, CDK, Serverless, Vercel, Netlify, Wrangler, Azure Functions, Kamal |
그것이 하지 않는 일은 이들을 비교하는 것입니다. 그것이 여기서의 전부이며, noir를 변경할 필요가 없습니다 — alibi는 관점당 한 번씩 실행하고 결과를 조인합니다.
관점별 부분이 중요합니다. Noir는 모든 분석기에 걸쳐 (method, url)로 중복을 제거하므로, 동일하게 표기된 Flask 라우트와 OpenAPI 경로는 하나의 기술을 가진 하나의 엔드포인트로 합쳐집니다. 이는 발견 도구로서는 옳습니다 — 하나의 엔드포인트이니까요 — 하지만 이 도구가 측정하려는 상호 입증을 지워버리며, 최악의 방향으로 지웁니다: 두 관점이 더 잘 일치할수록 더 많은 것이 사라집니다. Casdoor는 372개의 코드 엔드포인트와 9개의 문서화된 엔드포인트로 스캔됩니다; swagger/ 디렉터리만 스캔하면 명세에는 235개가 있습니다.
--only-techs는 탐지기 풀을 제한하므로, 관점당 한 번의 스캔이 각 관점을 온전하게 유지합니다. 어떤 기술이 어떤 관점을 대변하는지는 views.yml에 있고, 어떤 기술이 존재하는지는 noir list techs가 보고하는 대로입니다.
alibi는 자체적으로 API 형식을 파싱하지 않습니다. 유일한 입력은 noir의 JSON입니다.
PATH에 noir 1.0.0 이상이 필요합니다 -- noir list techs가 하위 명령이 된 릴리스이며, 그 카탈로그가 모든 기술을 관점에 할당합니다. 개발은 현재 noir 릴리스를 추적합니다. 구버전 바이너리는 첫 카탈로그 읽기에서 실패하도록 두지 않고 이름으로 거부됩니다.
$ 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
모든 경로는 소스이며, 관점당 한 번 스캔됩니다. 가지고 있는 것 — 소스 트리, 명세 디렉터리, 단일 캡처 파일 — 을 가리키면, 빠진 관점들은 보고서를 범람시키는 대신 해당 규칙을 꺼버립니다.
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.
그룹은 열두 개에서 멈춥니다 — 정렬은 최악 우선이므로 꼬리 부분이 가장 정보가 적고, -f json에 전부 있습니다.
- run: alibi scan . ./contracts -f sarif > alibi.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: alibi.sarif }
보고서는 관점들이 불일치한다고 말합니다; --endpoints는 각 관점이 무엇을 담고 있었는지 말합니다.
$ alibi scan ./repo -f json --endpoints
모든 관점이 목록을 받습니다: 키, 어떤 관점들이 그것을 입증했는지, 그 뒤의 기술들, 파일들, 그리고 정규화 전의 표기 — 두 행이 일치했어야 하는데 일치하지 않았을 때 차이는 항상 여기에 있습니다. 이는 나머지 페이로드의 3~4배이므로 기본값이 아니라 플래그입니다.
또는 직접 게이트할 수 있습니다: alibi scan . ./contracts --fail-on high는 발견 사항이 해당 심각도에 도달하면 비영(非零)으로 종료합니다. noir가 전체를 읽지 못한 스캔은 executionSuccessful: false를 보고하므로, 성능이 저하된 실행이 깨끗한 실행으로 통과하지 않습니다.
Noir는 공통된 하나를 만들어내는 대신 각 프레임워크의 자체 라우트 문법을 유지하므로, 동일한 엔드포인트가 여러 방식으로 표기되어 도착합니다:
python_flask /api/users/<int:user_id>
aiohttp /users/{id}
java_spring /api/catalog/{id}
oas3 /v1/pets/{petId}
rails /posts/:id
nginx /admin/.*
이들을 비교 가능하게 만드는 규칙: 경로 매개변수의 이름은 그 정체성의 일부가 아닙니다. {petId}와 <int:user_id>는 동일한 슬롯을 설명합니다; 오직 그 위치와 /를 가로지르는지 여부만 중요합니다. 이름은 증거로 보존되고 보고되지만, 키에는 결코 도달하지 않습니다.
발견 사항은 매칭이 어떻게 이루어졌는지 말합니다:
| 등급 | 의미 |
|---|---|
G1 | 표기가 이미 일치함 |
G2 | 매개변수 문법이 정규화되면 일치함 |
G0 | 한 관점만 가지고 있음 — 아무것도 매칭되지 않음 |
이런 도구는 첫 실행에서 수백 개의 발견 사항을 보고하거나, 아무도 이루지 않은 진전을 보고함으로써 죽습니다. 여섯 가지가 반박합니다:
규칙은 양쪽 관점이 없으면 발동하지 않습니다. 계약이 어디에도 없는 코드베이스를 스캔하면 모든 엔드포인트가 기술적으로 문서화되지 않은 섀도 API에 해당합니다. 그런 발견 사항은 문서를 제공하지 않았다는 것 외에는 아무것도 말하지 않으므로, 규칙은 그것이 추론하는 모든 관점이 실제로 스캔에 있었을 때만 실행됩니다. 보고서는 빠진 규칙들의 이름을 밝힙니다.
근접 실패는 발견 사항이 아니라 의심으로 보고됩니다. "코드에는 있고 문서에는 없음"은 "양쪽에 있지만 alibi가 정렬에 실패함"과 구별할 수 없습니다. 그래서 한 관점에 속한 엔드포인트는 다른 관점들에 대해 근접 실패가 있는지 확인됩니다 — 다른 동사로 같은 경로, 또는 한쪽은 매개변수를 가지고 다른 쪽은 리터럴을 가지는 한 세그먼트 차이. 근접 실패를 가진 발견 사항은 강등되고 검토용으로 표시됩니다. 그 수는 총계 옆에 놓입니다, 왜냐하면 모든 발견 사항은 그것이 작은 만큼만 신뢰할 수 있기 때문입니다.
만난 적 없는 관점들은 수백 개의 발견 사항이 아니라 하나의 진단입니다. Argo CD는 Go에서 /api를 등록하고 그 아래 198개의 경로를 문서화하므로, 그 코드와 명세는 단 하나의 엔드포인트도 공유하지 않습니다. 문자 그대로 읽으면 58개의 섀도 API와 198개의 유령 계약이며, 그중 실제는 없습니다. 채워진 두 관점 사이의 제로 상호 입증은 비교가 작동하지 않았다는 뜻입니다 — 라우트 아래의 경로들을 대신하는 마운트 지점, 또는 noir가 읽지 못한 스택 — 그래서 규칙들은 보류되고 그 이유가 대신 출력됩니다. 다른 관점들의 많은 엔드포인트가 그 아래에 있는 것으로 밝혀진 경로들은 추정 마운트로 표시됩니다.
두 관점이 한쪽에서 상수 접두사가 떨어져 나가면 정렬될 때, 진단은 그렇게 말하고 그 접두사의 이름을 밝힙니다. Gitea의 생성된 명세는 basePath: /GITEA-API-APP-SUBURL/api/v1을 선언하는 반면 그 Go 라우터는 /api/v1을 마운트합니다; 관점들은 아무것도 공유하지 않지만, 535개의 문서화된 경로 중 154개가 그 세 세그먼트를 제거하면 코드 경로와 일치합니다. 그것은 명세의 basePath, servers[].url, 또는 코드 리더가 떨어뜨린 마운트입니다 — 그리고 그것은 보고되며, 결코 적용되지 않습니다, 왜냐하면 경로를 재정렬하면 그것이 찾아낸 버그를 숨기게 되기 때문입니다.
정말로 하나의 빠진 하위 트리인 범람은 하나로 명명됩니다: NodeBB의 354개 유령 계약 중 207개가 /api/v3 아래에 있으며, 코드 관점은 거기에 아무것도 담고 있지 않습니다.
빠진 관점과 빈 관점은 반대를 의미합니다. Noir는 읽지 못한 것을 보고하고, alibi는 그것을 발견 사항 위에 출력합니다. NetBox는 308개 경로를 가진 12.35MB의 OpenAPI 문서를 제공합니다; noir는 파일 크기 상한을 초과하여 그것을 건너뛰고, 그 보고가 없으면 alibi는 프로젝트가 아무것도 문서화하지 않는다고 진술합니다 — 단지 불완전한 것이 아니라, 잘못된 답을 자신 있게 진술하는 것입니다.
실행을 멈춘 규칙은 아무것도 해결하지 않았습니다. 스캔을 기록하고 비교하는 것은 같은 실수를 멀리서 다시 들여옵니다: 한 실행에서 contracts 디렉터리를 잊으면 SHADOW는 아무것도 평가하지 않으며, 순진한 차이 비교에는 모든 섀도 API가 닫힌 것과 정확히 똑같이 보입니다. 5관점 픽스처에서, 인자 하나를 떨어뜨리면 일곱 개의 상존 발견 사항이 "해결됨"으로 바뀌었습니다. 스냅샷은 어떤 규칙이 평가되었는지 기록하고, 차이는 두 스캔 모두에서 실행된 규칙만 고려하며, 나머지는 NOT COMPARED 아래에 명명됩니다.
부재는 신호가 존재할 때만 증거입니다. Noir의 인증 태거는 그것이 아는 프레임워크를 커버합니다. 그것이 커버하지 않는 스택에서는 아무것도 인증 태그를 가지지 않으며, 그것을 "인증되지 않음"으로 취급하면 모든 발견 사항을 승격시키고 심각도 열을 의미 없게 만듭니다. 빠진 태그에 발동하는 조정은 그 태그가 스캔 어딘가에 먼저 나타날 것을 요구합니다.
그런 다음 심각도는 noir의 태거가 찾은 것에 따라 이동합니다: 개인 데이터, 파일 업로드, 인증의 흔적 없음, 또는 상태를 변경하는 메서드.
관점 맵(views.yml)과 규칙(rules.yml) 모두 코드가 아니라 데이터입니다.
하나의 location /api/는 그 아래의 모든 것을 대변하므로, 게이트웨이와 인프라 규칙은 이것이 그것을 담고 있는가가 아니라 이것이 그 엔드포인트에 도달하는가에 답합니다. 집합으로 비교하면, 모든 접두사 규칙은 아무도 구현하지 않은 라우트처럼 보이고 모든 구현된 라우트는 도달 불가능해 보입니다.
커버리지는 의도적으로 관대합니다. Noir는 규칙이 매칭하는 경로를 보고하지만 접두사로 매칭하는지 정확히 매칭하는지(location = /x, Ingress pathType: Exact)는 보고하지 않으므로, 정확성은 복구될 수 없습니다 — 그리고 모든 규칙을 접두사로 취급하는 것은 발견 사항을 만들어내는 대신 억제합니다.
보고서는 각 라우팅 관점이 코드의 얼마나 도달하는지 말합니다, 왜냐하면 "어떤 게이트웨이도 도달하지 않는 34개 엔드포인트"가 실제인지는 그 구성이 서비스를 앞단에서 처리하는 것인지에 달려 있기 때문입니다. 어떤 임계값도 그것들을 정직하게 구분하지 못합니다: Argo CD의 e2e 테스트 픽스처는 코드의 39%에 도달하고 NetBox의 실제 구성은 100%에 도달합니다.
포괄 규칙 — location /, /의 Ingress, RewriteRule ^(.*)$ — 은 어느 쪽으로도 증거가 아닙니다. 그것은 모든 것을 라우팅하거나 아무것도 라우팅하지 않으며, 모든 엔드포인트에 대해 동일하므로, 어느 것에도 도달하지 않는 것으로 계산됩니다. 다른 것을 담고 있지 않은 게이트웨이 관점은 제공할 신호가 없으며, UNEXPOSED는 모든 엔드포인트를 도달 불가능으로 보고하는 대신 빠지고 그렇게 말합니다. Casdoor의 Helm 차트가 정확히 그렇습니다: /에 하나의 Ingress 규칙이 있으며, 증거로 읽으면 365개의 발견 사항을 만들어냈습니다.
HAR 캡처는 일어난 요청을 기록합니다. Postman 컬렉션은 누군가 만들려고 했던 요청을 기록합니다. ORPHAN, LIVE_UNDOC, COLD는 모두 무엇이 실행되었는지에 대해 추론하므로, 누군가 실제로 관찰한 관점을 요구하며 빠질 때 그렇게 말합니다.
Noir는 CLI 인자, Kafka 토픽, 모바일 딥 링크를 같은 목록에 보고합니다. HTTP로 평탄화된 cli://gitops-engine/agent는 /agent가 됩니다 — 그것은 그 이름의 어떤 웹 라우트와도 충돌하고 게이트웨이가 그것으로 라우팅하는지 질문받습니다. 프로토콜은 엔드포인트의 정체성에 속합니다; http와 https는 하나의 공간이고 나머지는 모두 자기 자신을 유지합니다.
일부 간극은 의도된 상태입니다. 소스 옆에 .alibi.yml을 두십시오:
ignore:
- path: "^/internal/"
why: internal-only admin surface
- rule: UNEXPOSED
path: "^/debug/"
why: not fronted by the gateway in this repo
또는 일회성으로 --ignore REGEX를 전달하십시오. 억제된 발견 사항은 집계되고 그 수가 출력됩니다 — 발견 사항을 조용히 버리는 도구는 너무 많이 출력하는 도구보다 나쁩니다, 왜냐하면 무엇을 보류했는지 알 방법이 더 이상 없기 때문입니다.
초기이지만, 다섯 개의 관점 모두 비교됩니다.
다섯 개 저장소에 대해 측정:
Casdoor가 가장 깨끗한 사례입니다: 문서화된 235개 엔드포인트 중 230개가 코드와 일치했으며, 경로 정규화 실패가 전혀 없었습니다. 19개의 근접 실패는 모두 다른 동사 아래의 같은 경로였습니다 — noir가 Go catch-all 핸들러의 모든 메서드를 등록한 것이지, 매칭 문제가 아닙니다.
NetBox가 교훈적인 사례입니다. 그것은 하나의 저장소에 두 개의 표면을 담고 있습니다: 서버 렌더링 웹 UI와 두 번째만 문서화된 DRF-router REST API. 전체를 스캔하면 746개의 발견 사항을 보고하며, 대부분은 웹 UI가 API 명세에 없다는 진실이지만 쓸모없는 관찰입니다. 계약이 설명하는 표면으로 범위를 좁히면, 실제로 말할 가치가 있는 것으로 축소됩니다:
$ alibi scan ./netbox --ignore '^/(?!api(/|$))'
→ 3개의 섀도 API: /api/plugins, /api/schema/redoc, /api/schema/swagger-ui, 세 개 모두 진정으로 서비스되고 진정으로 스키마에 없습니다. 남아 있는 397개의 유령은 NetBox 자체의 라우터 하위 클래스가 모든 목록 엔드포인트에 추가하는 대량 작업이며, 어떤 urlconf 순회로도 볼 수 없습니다.
나머지 세 개는 보류되며, 각각 알 가치가 있는 이유가 있습니다:
/api를 등록하고 그 아래 198개의 경로를 문서화합니다 — 두 가지 세분성의 같은 표면입니다.urls 모듈을 임포트하여 런타임에 URLconf를 조립하며, 어떤 정적 리더도 따라갈 수 없습니다..proto 어노테이션으로 구현되며, 이것이 grpc가 코드 관점을 대변하는 이유입니다: 거기에 분류하면, 문서화된 36개 경로 중 36개가 상호 입증됩니다.이것이 이 도구의 한계이며, 명확히 말하면: 그것은 noir가 읽을 수 있는 것을 비교하고, 잘못된 세분성으로 읽은 관점은 전혀 읽지 않은 것보다 나쁩니다. 위의 기계 장치 대부분은 그것들을 결함으로 보고하는 대신 구별하기 위해 존재합니다.
MIT
| 규칙 | 조건 | 심각도 |
|---|
ORPHAN | 실제 요청을 받지만, 코드에 없음 | high |
LIVE_UNDOC | 실제 요청을 받지만, 어떤 계약도 설명하지 않음 | high |
SHADOW | 코드에 있지만, 어떤 계약에도 없음 | medium |
DANGLING | 구현된 것에 도달하지 않는 게이트웨이 규칙 | medium |
DRIFT | 배포를 위해 선언되었지만, 코드에 없음 | medium |
PHANTOM | 계약에 있지만, 코드에 없음 | low |
UNEXPOSED | 구현되었지만, 어떤 게이트웨이 규칙도 도달하지 않음 | low |
COLD | 구현되었지만, 요청을 받는 것이 결코 관찰되지 않음 | info |
| 저장소 | 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 |