Blastradius를 이용한 그래프 해킹 방사반경 구축
Blastradius tracing a critical advisory through the dependency graph to an internet-facing service
실시간 데모: https://13-203-50-195.sslip.io · 코드: https://github.com/NishaadG/blastradius
새로운 CVE(Common Vulnerabilities and Exposures)가 나올 때마다, 사람들은 같은 네 가지 질문을 합니다: 우리가 영향을 받았는가? 어떤 종속성(dependency)을 통해? 누가 그 서비스를 소유하는가? 이런 종류의 문제는 이미 처리한 적이 있는가? 대부분의 도구는 목록으로 답합니다. 하지만 정직한 대답은 _경로(path)_입니다.
"유사 텍스트"의 문제점
대부분의 에이전트 데모는 임베딩 유사성(embedding similarity)을 통해 컨텍스트를 검색합니다. 이것은
Log4Shell에 대해 물어보고 같은 쿼리가 0개의 행을 반환하는 경우를 생각해 보세요. 그것은 실패가 아니라 답변입니다. 그래프의 어떤 것도 Maven 패키지에 의존하지 않기 때문에 판결은 NOT_AFFECTED이며, 화면상의 빈 순회(traversal)가 증거입니다. 실제로 실행하는 패키지에 대한 권고 사항을 물어보되, 패치된 버전으로 물어보면, 그래프는 왜 안전한지 설명해 줍니다. 즉, 해당 패키지는 존재하고 버전이 영향을 받는 범위 밖에 있다는 것입니다.
LLM은 결정권을 갖지 않는다
판결(AFFECTED / NOT_AFFECTED / NEEDS_HUMAN)은 그래프 증거에 대한 단순 규칙에서 나옵니다. LLM(선택 사항이며 무료 등급)은 어떤 도구를 호출할지, 그리고 결과를 어떻게 문장화할지만 선택합니다. 데이터베이스 자격 증명은 절대 보지 못하며 Cypher를 작성하지도 않습니다. 읽기 경로는 쓰기에 대해서 거부되고 데이터베이스에 의해 강제적으로 읽기 전용으로 유지됩니다. 복구 조치(Remediation actions)는 인간의 승인을 기다리는 제안일 뿐입니다.
그래프인 기억력
사람이 수정 사항을 승인하면, 그 결정은 Case 노드로 작성되어 권고 사항, 영향을 받는 서비스, 사용된 플레이북에 연결됩니다. 며칠 후 다른 protobufjs 권고 사항이 도착하고, 세 단계 순회((Case)-[:ABOUT]->(Advisory)-[:AFFECTS]->(Package))를 통해 이전 케이스를 찾습니다. 에이전트는 이를 인용하고 플레이북을 재사용하며, 플레이북의 성공 카운터가 올라갑니다. 기억력, 의존성 데이터, 소유권이 하나의 그래프에 존재하므로, 에이전트 간의 인계(Step-[:HANDS_OFF_TO]->Step) 역시 쿼리할 수 있습니다.
각 테넌트는 자신만의 FalkorDB 그래프를 가지므로, 격리는 WHERE tenant = …와 같이 사람이 잊어버릴까 봐 걱정하는 방식이 아니라 구조적입니다.
영수증을 갖춘 회사 두뇌
소유권은 CODEOWNERS에서, 전문 지식은 커밋 기록에서, 의존성 결정은 그것들을 만든 정확한 커밋에 연결됩니다. SUPERSEDES 엣지는 나중에 대체된 결정을 표시하며,
커밋은 무엇이 변경되었는지만 말할 뿐, 그 이유를 말하지 않기 때문에, 빌드 단계는 각 의존성 변경 뒤에 있는 풀 리퀘스트(pull request)도 읽고 Gemini가 결정 사항과 명시된 이유를 추출합니다. 모델은 제한됩니다: PR에서 언급한 내용만 다시 말할 수 있고, 빈 근거(rationale)는 비어 있게 유지되며, 패키지는 이름이 PR 또는 커밋 텍스트에 문자 그대로 나타날 때만 결정 사항과 연결됩니다. 두 가지 평가(evals)가 이를 강제하며, 두 번째 평가는 모델이 첫 실행에서 주체(otlp-exporters, 패키지가 아닌 의역)를 지어내는 것을 포착했습니다. 이제 '왜 golang.org/x/net을 업그레이드했는가?'라는 질문에 CVE와 PR 링크로 답변합니다.
토폴로지는 발견되는 것이지, 입력되는 것이 아니다
초기에는 어떤 서비스가 어떤 서비스를 호출하는지 제가 직접 작성했습니다. 이것이 모든 데모의 가장 약한 부분이므로, 저는 이를 교체했습니다: Blastradius는 이제 프로젝트 자체의 배포 파일들을 읽습니다. Online Boutique의 Kubernetes 매니페스트에서 PRODUCT_CATALOG_SERVICE_ADDR=productcatalogservice:3550은 호출이며, LoadBalancer 서비스는 인터넷 노출입니다. OpenTelemetry 데모의 Compose 파일에서는 순수한 환경 변수 이름이 .env를 통해 해결되고, 실제 host:container 포트 매핑만이 노출로 간주됩니다. 제가 예전에 직접 작성한 목록과 비교했을 때, 발견 기능은 OpenTelemetry 엣지(edges)의 100%를 복구했고 (문서에는 언급되지 않은 7개를 찾아냈습니다), Bank of Anthos의 100%(주소는 ConfigMaps와 jdbc: URL 내에 숨겨져 있음) 및 Boutique의 94%를 발견했습니다. 놓친 하나는 어떤 배포 파일에도 없는 것이며, 평가 보고서도 그렇게 말합니다.
매니페스트가 숨기는 경로를 포함한 모든 공개 저장소
owner/name을 붙여넣으면 Blastradius가 매니페스트를 찾아내고, 매니페스트에 직접 의존성만 나열된 경우 deps.dev에 해결된 의존성 트리를 요청하여 테넌트 그래프(tenant graph)를 구축합니다. 고의적으로 취약하게 만든 Java 앱을 가리키자, app → spring-boot-starter-log4j2 → [email protected]로 Log4Shell을 찾아냅니다. 빌드 파일에는 log4j-core가 언급되지 않았고, 정적 파서는 아무것도 보지 못합니다. 해결된 의존성의 그래프는 전체 체인을 봅니다.
하나의 그래프, 여러 AI 클라이언트
동일한 툴킷은 MCP 서버로 노출되므로 Claude Desktop, Claude Code 또는 모든 MCP 클라이언트가 그래프에 직접 질문하고 채팅 상자용 툴 호출 루프(tool-calling loop)를 사용할 수 있습니다. 모델은 검증된 읽기 전용 도구만 호출할 수 있으며 Cypher는 절대 볼 수 없습니다.
어떻게 이것이 정확한지 알까요?
진실 값(Ground truth)은 원본 lockfile, 캐시된 OSV 데이터, 발견된 토폴로지 및 CODEOWNERS로부터 그래프 없이 계산됩니다. 그런 다음 세 가지 실제 프로젝트(OpenTelemetry 데모, Online Boutique, Bank of Anthos)의 모든 권고에 대한 영향받는 서비스 집합과 판결을 제품과 비교합니다. 이 과정에서 반환되는 모든 경로는 lockfile 엣지, 노출 거리, 소유권, 전이 호출자, 메모리 재사용 및 테넌트/세션 격리의 실제 체인임을 확인했습니다. 57개 중 57개가 통과했으며, 42개의 단위 테스트와 가드레일 검사(에이전트 경로에서는 쓰기 쿼리가 거부됨)도 통과했습니다.
부하 상태에서 솔직하게
Track 2는 높은 동시성 공유 상태를 언급했으므로, 제가 이를 벤치마킹했습니다. 10개, 50개, 100개의 에이전트가 하나의 그래프에 집중적으로 접근하도록 했습니다. 읽기 작업은 빠르게 유지되었지만, FalkorDB는 쓰기를 직렬화(serialises)하므로 쓰기 지연 시간이 대기열을 만듭니다. 100개 에이전트에서 첫 번째 실행 시 약 2%의 작업이 Max pending queries exceeded로 거부되는 것을 확인했습니다. 이는 FalkorDB가 자신을 보호하는 것이므로, 클라이언트는 이제 지터링 백오프(jittered backoff)를 사용하여 재시도하고 숨기지 않고 모든 재시도를 결과에 포함합니다.
워처(watcher)는 일정에 따라 OSV를 재쿼리하고 그래프가 구축된 이후 게시된 권고 사항을 플래그 지정합니다. CLI와 GitHub Action은 체크아웃을 분석하여 SARIF 파일을 작성하고, 베이스 브랜치 대비 변경되는 발견 사항이 있는 경우에만 풀 리퀘스트(pull request)를 실패시킵니다.
SBOM 입력, SBOM 출력
모든 팀이 도구를 소스 코드를 향하게 할 수는 없습니다. 그래서 Blastradius는 CycloneDX 또는 SPDX 파일을 받기도 합니다: 각 구성 요소의 purl은 Package 노드가 되고, SBOM의 의존성 목록은 DEPENDS_ON 엣지가 되며, 그로부터는 동일한 그래프와 동일한 순회(traversal)를 수행합니다. 반대 방향도 마찬가지입니다: 모든 테넌트가 적용되는 권고 사항과 함께 CycloneDX 1.5로 내보냅니다. eval은 세 가지 프로젝트 각각을 내보내고 파일을 재가져와 패키지와 엣지가 동일하게 돌아오는지 확인합니다.
낯선 사람들과 공유하는 무료 등급 모델
호스팅된 데모는 Gemini의 무료 등급에서 실행되며, 방문자 한 명이 할당량(quota)을 공유합니다. 바쁜 몇 분이 오류로 바뀌는 대신, 서버는 모든 사람에 걸쳐 모델 호출을 예산 책정하고 실패 후 잠시 동안 작동을 멈춥니다. 모델이 다운되면 동일한 도구들이 결정론적 라우터(deterministic router)를 통해 응답하며, UI는 어떤 도구가 왜 응답했는지 보여줍니다. 판결이 결코 모델에 의존하지 않았기 때문에,