분석

리더보드를 읽는 데 필요한 규칙과, 숫자를 해석하는 근거를 모았다.

지표와 채점 규칙

지표

도구가 낸 결과를 사람이 큐레이션한 정답과 대조해 잰다. 정확도 지표 다섯 가지와, 이번 회차에 생긴 도달성과 AI 모델 파일 지표가 있다.

정밀도
도구가 낸 것 중 맞은 비율. 과탐이 많으면 하락
재현율
정답 중 찾은 비율. 미탐이 많으면 하락
F1
정밀도와 재현율의 조화평균. 도구 비교의 기준
라이선스 일치율
도구가 찾은 컴포넌트 중 정답 라이선스를 계열 수준에서 맞춘 비율. 찾지 못한 컴포넌트는 컴포넌트 축에서 감점하므로 여기서는 세지 않는다
대응
정답 취약점 중 고칠 버전까지 제시한 비율
호출 취약점 유지
도달성. 실제로 호출되는 취약점(affected) 중 도구가 영향 있음으로 남긴 비율. 1 미만이면 실제 위험을 숨긴 것이다
미호출 취약점 거름
도달성. 호출되지 않거나 링크되지 않은 취약점(not_affected) 중 도구가 거른 비율. 그 패키지를 찾았을 때만 인정한다
모델 검출, 형식 식별
AI 모델 파일. 모델을 구성요소로 찾은 비율과 형식까지 맞힌 비율
위험 직렬화 탐지
AI 모델 파일. 로드할 때 코드가 실행되는 파일 중 위험으로 표시한 비율
해석 예
정답 31개 중 3개만 찾고 그 3개가 다 맞으면 정밀은 3/3 이지만 재현은 3/31 이고 F1 은 18%. 정밀만으로는 도구를 비교할 수 없다
표본 크기
표에는 비율 대신 개수를 적는다. 정답이 몇 개인지 보이지 않으면 3개 중 3개와 42개 중 42개가 똑같이 100% 로 읽힌다

SBOM 품질

CISA 2026 SBOM 최소 요소(문서 버전 2.1, 2026-07-29)의 데이터 필드를 도구 출력이 얼마나 채우는지 잰다. 정답과 대조하지 않는 결정적 검사라 정확도 지표와 섞지 않는다.

대상
CycloneDX 를 내는 도구의 원시 출력. 다른 형식(grype, osv-scanner, dependency-check 의 자체 JSON)은 재지 않는다
메타 항목
작성자, 작성자 서명, 데이터 형식, 생성 맥락(lifecycles), 시각, 도구 이름, 도구 버전, SBOM 버전. 문서마다 있음과 없음
구성요소 항목
생산자(supplier 또는 manufacturer), 해시(알고리즘과 값), 식별자, 식별자 문법(purl-spec), 라이선스, 이름, 버전, 의존 관계. 구성요소 중 채운 비율
종합
16개 항목의 평균. CISA 17개 필드에서 데이터 형식 이름과 버전, 해시 값과 알고리즘을 각각 한 항목으로 묶고 식별자 문법을 더했다
한계
채웠는지만 보고 값이 맞는지는 보지 않는다. 값의 정확도는 컴포넌트, 라이선스 지표가 잰다. 파일 항목(type file)은 구성요소에서 뺀다. 구성요소가 하나도 없는 출력은 잴 대상이 없어 품질 집계에서 뺀다(구성요소를 못 낸 것은 정확도 지표가 잡는다)

정답 기준

정답은 배포물의 실제 빌드 결과다.

근거
SBOM 의 목적이 배포한 소프트웨어에 실제로 무엇이 들어있는지 알아 취약점에 대응하는 것
큐레이션
빌드 타깃의 실제 링크와 설치 결과를 사람이 확인해 작성. 플랫폼 조건부로 링크되지 않는 것은 제외
낮은 정밀도
선언만 되고 실제 빌드에 안 들어가는 것까지 내면 정밀도가 낮아진다. 도구 결함이 아니라 소스에 적힌 대로 낸 것이며, 목적 기준으로는 정당한 평가다
취약점 정밀도
정답이 그 검증 대상의 적용 CVE 를 전부 담을 때만 채점한다(vulnComplete). 아니면 재현율만 본다

도구 구분과 재현성

도구가 무엇이고 결과가 어디서 왔는지로 셋으로 나눈다. 재현 가능성은 여기서 따라온다.

오픈소스
우리가 직접 실행한다. 누구나 같은 버전으로 재현할 수 있고, 공개 리더보드에는 이것만 싣는다
소스 비공개 도구
소스가 공개되지 않은 도구(상용 제품, 무료로 제공되지만 소스가 닫힌 서비스). 계정이나 라이선스 보유자가 자기 환경에서 실행한다. 외부 재현이 어렵고 결과는 공개하지 않는다
벤더 제출
벤더가 제출한 원시 출력을 벤치마크 코드로 재채점한다. 도구 자체는 오픈소스여도 실행을 우리가 하지 않아 독립 재현은 안 된다

표기

개수를 주 표시로 쓰고, 개수로 적을 수 없는 자리에만 퍼센트를 쓴다.

개수를 쓰는 이유
정답이 1~3개인 검증 대상이 20개 중 11개다. 퍼센트로 적으면 표본 크기가 지워져 '1개 중 1개'가 100% 로 읽히고, 도구가 일반적으로 완벽하다는 인상을 준다
검증 대상별 표
맞은 개수/전체 개수로 적는다. F1 만 퍼센트다 — 정밀도와 재현율을 합친 파생 지표라 개수로 표현할 수 없다
카테고리 표
여러 검증 대상을 합치는 자리라 퍼센트를 쓰되, 그 퍼센트가 걸친 정답 개수를 함께 적는다
0%
측정 대상인데 결과가 없음
—
측정 대상이 아니거나 잴 대상이 없음(검출 0건의 정밀도 등)
⚠
실행 오류로 점수를 내지 못함. 평균에서 제외
종합
축 평균들의 평균. 축마다 검증 대상 수가 달라도 축을 동일 가중한다. 검증 대상 수는 벤치마크 커버리지의 속성이지 도구 품질이 아니다

점수의 원인

여러 검증 대상과 도구에서 반복되는 원인을 한 곳에 모았다. 리더보드의 드릴다운이 해당 항목으로 링크한다.

매니페스트 파서: 못 푸는 것과 과포함

선언 파일과 lockfile 을 읽는 도구는 두 방향으로 실제 빌드 결과와 어긋난다.

대상
syft, trivy, grype 등 파일을 읽는 도구
못 푸는 쪽
추상 범위(py-range 의 flask>=2.0)나 빌드로만 간접 의존성이 풀리는 Gradle(gradle-nolock)에서 버전이 확정되지 않아 컴포넌트를 못 낸다. 재현율 0
과포함 쪽
선언만 되고 실제 빌드에 링크되지 않는 것(go-mod 의 windows 전용 mousetrap)까지 내 정밀도가 낮아진다
대비
설치와 빌드를 해석하는 bomlens, cdxgen 은 양쪽에서 유리하다. osv-scanner 는 정적 범위 해석

go-mod: 정답은 실제 빌드 결과

빌드 타깃 linux/amd64 에 실제 링크되는 것만 정답으로 잡았다.

정답
cobra, pflag. mousetrap 은 cobra 가 windows 에서만 import 하는 전용 간접 의존성이라 제외
검증
GOOS=linux go list -deps 로 확인하고, windows 빌드에는 포함됨을 대조
결과
go.sum 선언에는 mousetrap 이 있어 매니페스트 파서가 과포함(syft, osv-scanner 정밀도 0.667, trivy 0.5). 빌드를 해석하는 cdxgen, bomlens 는 정밀도와 재현율 모두 1
함의
SBOM 의 목적이 배포물 구성이므로 후자가 옳다. 목적 기준 채점의 변별이 드러나는 자리

lockfile 이 있으면 도구가 수렴한다

간접 의존성까지 버전이 고정돼 있으면 대부분 도구가 같은 결과를 낸다.

관찰
node-lock, gradle-lock 에서는 매니페스트 파서도 간접 의존성까지 잡는다
함의
도구 차이는 lockfile 이 없는 입력에서만 드러난다

gradle BOM constraint 과탐 (jackson-bom)

의존성 관리용 메타데이터를 실제 컴포넌트로 올리는 과탐이다.

관찰
빌드형 도구가 jackson-bom 을 컴포넌트로 내 정밀도가 0.75 로 하락
원인
Gradle 의존성 그래프에 BOM(Bill of Materials) constraint 가 들어 있다
정답
BOM 은 의존성 관리 메타데이터로 보고 제외
범위
gradle fixture 의 빌드형 도구 공통

grype 의 컴포넌트는 취약점 매칭의 부산물

grype 는 SBOM 생성기가 아니라 취약점 매처다.

원인
출력에 전체 artifacts 가 없고 matches(취약점)만 있어, 어댑터가 취약점 매칭된 패키지만 컴포넌트로 잡는다
결과
container-image 에서 syft, trivy 는 15/15 를 내지만 grype 는 6/15

컨테이너 취약점: 배포판 보안 데이터베이스와 상위 버전 매칭

배포판 패키지는 버전을 올리지 않고 취약점만 고치는 백포트 패치가 흔하다.

갈림
배포판 보안 데이터베이스를 따르는 도구는 백포트를 반영해 정확하고, 상위 버전 범위로 매칭하는 도구는 이미 고쳐진 CVE 까지 보고해 과탐한다
Alpine
container-image 에서 trivy 0건 대 grype 14건. 이 차이를 처음 관찰한 자리
Debian
image-debian-backport 에서는 둘 다 tracker 계열 데이터를 써 trivy 와 grype 모두 42/42(정밀도, 재현율 1.0). 대응까지 백포트 리비전(3.0.20-1~deb12u2)으로 정확히 제시

osv-scanner 의 Debian 측정: 캐시 한계에서 만점으로

1회차에는 측정 자체가 불가능했고, 2회차 동결에서 만점이 됐다.

1회차
2026-06-30 스냅샷에서는 오프라인 캐시에 Debian 생태계가 없어 측정 불가. 도구 결함이 아니라 동결 캐시의 한계
2회차
2026-07-03 동결에서 refresh 가 image fixture 생태계까지 적재하도록 개선
정규화
배포판 레코드 식별자(DEBIAN-CVE-*, DSA 의 상위 프로젝트 연결)를 CVE 로 환원
결과
취약점 정밀도와 재현율 1.0, 대응 1.0 으로 배포판 보안 데이터베이스 계열과 나란하다. 컴포넌트는 소스 패키지 이름 보고 특성으로 0.667
2026-10 회차
65건 정답에서도 정밀도와 재현율 1.0. OSV 가 서로 다른 GHSA 를 별칭으로 묶는 경우가 생겨, 어댑터가 레코드 별칭의 CVE 를 모두 반영하도록 고쳤다

bomlens 의 OS 패키지 취약점: 0 에서 전수 검출로

벤치마크의 이슈 보고로 결함 두 건이 차례로 규명, 수정됐다.

최초 관찰
image-debian-backport 에서 bomlens 취약점 0. 독립 실행한 trivy 는 42건
1차 원인
Trivy 가 배포판 보안 권고 조회용 소스 패키지 이름을 자기 SBOM 전용 속성(aquasecurity:trivy:SrcName)에서만 읽어 syft 계열 SBOM 재스캔이 조용히 0건. 정규화 단계에서 purl 로부터 속성을 주입해 수정(sbom-tools PR #311). alpine 경로도 같은 원인
2차 원인
파이프라인이 만드는 BOM 이 CycloneDX 1.7 인데 내부 Trivy 가 1.6 까지만 디코드해 실패가 경고로 삼켜짐. 1.7 디코드(PR #325)와 실패 가시성 개선(PR #326)으로 수정
결과
재스캔에서 42/42(정밀도, 재현율 1.0), 대응 1.0
2026-10 회차
정답이 65건으로 늘었고(Debian 추적기로 확인한 2026 CVE 23건 추가) 65/65 재현, 정밀도 0.985. 과탐 1건은 CVE 가 아닌 Debian LTS 권고 번호(DLA-4792-1)로, 같은 엔진의 trivy 도 같다
함의
내부 엔진이 같아도 실제 도구로 측정하는 원칙이 도구 개선으로 이어진 사례. 상세는 docs/bomlens-os-vuln-issue.md

도달성: 판정은 세 도구가 내고, 방식이 다르다

호출되지 않는 취약점을 거르는 도구는 osv-scanner, bomlens, grype 다.

osv-scanner
소스에서 Go 호출 분석(--call-analysis=go)으로 함수 단위 판정. 소스 두 fixture 모두 맞힘. 바이너리 입력은 받지 않는다
grype
바이너리에서 심볼과 링크된 패키지로 거른다. 바이너리 두 fixture 는 맞히고, 소스에서는 거르지 않는다
bomlens
--reachability 로 govulncheck 를 돌려, 소스는 호출 그래프로, 바이너리는 심볼로 판정한다. 네 fixture 모두 맞힘(1.13.4, 그 전 판은 판정 기능이 없었다)
판정 없음
trivy 는 보고한 것을 모두 영향 있음으로 낸다. 이 바이너리에서 Go 모듈을 찾지 못해 호출되는 취약점도 놓쳤다
채점 규칙
보고하지 않은 것을 거른 것으로 인정하려면 그 패키지를 찾았어야 한다(못 본 것은 미검출)

AI 모델 파일: pickle 계열에서 갈린다

GGUF, safetensors, ONNX 는 두 도구가 모두 읽고, pickle 과 PyTorch 파일에서 차이가 난다.

cdxgen
AI 유형(-t ai)에서 GGUF, safetensors, ONNX 의 형식과 메타데이터를 정확히 읽는다. pickle 과 PyTorch 파일은 구성요소로 내지 않아 모델 검출 0.50
bomlens
파일마다 --model-file 로 6종을 모두 식별하고, picklescan 결과로 위험 직렬화 2개를 unsafe, 일반 pickle 을 clean 으로 구분한다
정답 기준
형식이 pickle 이라는 사실만으로는 위험으로 보지 않는다. 로드할 때 코드가 실행되는 내용이 있어야 위험이다

syft 는 취약점을 내지 않는다

SBOM 생성 전용이라 취약점 축은 항상 0 이다.

선언
produces = component, license
취약점
trivy, grype, osv-scanner, dependency-check 등 매처의 몫

cdxgen 13.4.0 은 npm 설치 결과를 버전 없는 항목으로 한 번 더 낸다

lockfile 없는 npm 프로젝트에서 같은 패키지가 두 번 나온다.

현상
pkg:npm/debug@4.3.1 같은 정상 항목에 더해 pkg:generic/debug#node_modules/debug 처럼 버전 없는 항목 3개를 함께 낸다. 정밀도 0.5, F1 0.667(13.2.0 은 1.00)
판정
같은 패키지의 중복 표현이지만 도구가 낸 컴포넌트이므로 과탐으로 센다

CVE 가 아닌 식별자는 별칭으로 인정한다

같은 취약점을 다른 식별자 체계로 보고한 것을 미탐과 과탐으로 이중 벌점하지 않는다.

사정
일부 도구는 GHSA, NSWG-ECO-516 같은 식별자로 보고한다
규칙
정답의 각 CVE 에 사람이 큐레이션한 별칭(aliases)을 두고 별칭 일치를 정탐으로 인정
사례
npm-vuln-pinned 에서 dependency-check(NPM Audit 경로, GHSA 만 보고)가 재현율 1, 정밀도 1. 고칠 버전은 주지 않아 대응 0
한계
별칭이 확인되지 않는 식별자(NSWG 등)는 과탐으로 남는다. trivy, bomlens 0.857

BomLens 가 JVM 직접 의존성을 간접으로 오분류 (수정됨)

sbom-tools 에 전달돼 수정된 건이다. 아래는 이전 버전 관찰의 기록이다.

관찰
Maven, Gradle 에서 직접 의존성을 모두 transitive 로 표시. npm 은 정상이고 cdxgen 직접 실행도 정상
원인
build-prep 의 JVM 빌드 경로에서 scope 신호가 손실
영향
점수(버전 매칭)에는 무관하나 SBOM 의 scope 정확성에 영향
경과
재측정에서 gradle-nolock, java-maven 의 직접 의존성이 0에서 1로(jackson-databind, log4j-core). 상세는 docs/bomlens-jvm-scope-issue.md

같은 엔진의 일치는 독립 검증이 아니다

도구가 내부적으로 다른 도구의 엔진을 쓰는 경우가 있다.

bomlens
취약점은 Trivy native 출력에서 온다
grype
컴포넌트는 내장 syft 에서 온다
함의
같은 엔진을 공유하는 도구가 같은 결과를 내는 것은 교차 검증이 아니다. docs/methodology.md 참조

파일 단위 라이선스는 파일을 직접 스캔하는 도구만 검출한다

매니페스트가 없는 순수 소스는 파일 헤더의 라이선스를 읽어야 한다.

대상
소스코드 카테고리(src-licenses, license-mix)
검출
scancode(순수 파일 스캔), bomlens(--deep-license, 내부 ScanCode). 모두 1.0
미검출
syft, trivy, grype, osv-scanner, cdxgen, dependency-check 는 패키지 기반이라 컴포넌트가 0 이면 라이선스도 0
경과
bomlens 는 opt-in 이미지(SBOM_DEEP_LICENSE=true)가 빌드(libicu-dev 누락)와 실행(libarchive, libmagic 심볼)에서 깨져 실패했다. 이슈 보고 후 수정돼 재측정에서 정상 검출(docs/bomlens-deep-license-issue.md)
함의
내부 엔진이 ScanCode 라도 실제 도구로 측정했기에 통합 버그가 드러났다

bomlens 의 범위 의존성 이중 계상 (수정됨)

매니페스트 하한과 설치 버전을 병합 없이 둘 다 내던 문제다.

이전 관찰
flask>=2.0 에서 하한(flask@2.0)과 설치 버전(flask@3.1.3)을 둘 다 컴포넌트로 내고, 하한 구버전에 취약점을 매겨 과탐. BOM 근거상 두 버전 다 requirements.txt 출처
수정
설치 버전이 확정되면 versionSpecifiers 를 가진 매니페스트 하한을 제거하고 의존성 그래프를 정리한다. pkg:pypi 한정이고 npm, maven 의 다이아몬드 의존성은 보존
결과
py-range 컴포넌트 16에서 12로(하한 4개 제거), 취약점 21에서 0으로
상세
docs/bomlens-range-duplicate-issue.md

컨테이너 이미지 스캔: 커버리지와 purl 표기가 갈린다

같은 이미지를 두고 도구마다 검출 범위와 식별자 표기가 다르다.

완전
syft, trivy 가 apk 15/15. bomlens 는 내부 syft 로 동일하게 15/15(통합 이상 없음을 실측으로 확인)
과탐
cdxgen 은 재현율 1 이다. 이미지의 파일 항목(type: file)도 내지만 이번 회차부터 컴포넌트로 세지 않는다. 남은 과탐 4개는 설치된 패키지의 원본 소스 패키지(openssl 등)를 별도 구성요소로 낸 것이라 정밀도 0.79
분해 차이
osv-scanner 는 apk 를 syft 와 다르게 분해하고(libc-dev 대 libc-utils) purl 도 비표준(pkg:alpine:v3.18/)이라 6/15. 어댑터에서 표준 pkg:apk/alpine/ 로 정규화
완전(추가)
osv-scalibr 도 15/15
부산물
grype 는 취약점 매처라 취약점 있는 패키지만 6/15
미지원
dependency-check, scancode 는 이미지 입력을 지원하지 않아 이 카테고리에서 빠진다

더 보기

방법론, BomLens JVM scope 이슈, 도구 추가하기

입력 유형별 참조

자기 입력과 같은 검증 대상의 결과가 종합 순위보다 정확한 판단 근거다.

입력검증 대상변별점
소스코드
Java (Maven, Gradle)java-maven, gradle-lock, gradle-nolock빌드 해석 여부와 scope 구분. lockfile 이 없으면 크게 갈린다
Android (Gradle)android-app, android-flavor배포본에 실제 포함되는 의존성만 가려내는지
Node.js (npm)node-lock, npm-nolock, npm-vuln-pinnedlockfile 이 있으면 도구가 수렴하고, 없으면 설치 해석에서 갈린다
Node.js (pnpm 워크스페이스, Bun)pnpm-workspace, bun-lock새 lockfile 형식을 읽는지, 개발 의존성을 빼는지
예제, 테스트 디렉터리가 섞인 저장소npm-nonshipped배포되지 않는 매니페스트와 개발 의존성을 빼는지
Python (requirements.txt)py-pinned, py-range간접 의존성과 버전 범위 해석
Python (uv.lock)uv-lockuv lockfile 을 읽는지, 개발 의존성 그룹을 빼는지
PHP, Ruby (lockfile 없음)php-nolock, ruby-nolocklockfile 없이 의존성을 해석하는지, 개발 의존성을 빼는지
Go (go.mod)go-mod실제 링크와 선언 전체의 차이
Rust (Cargo.toml)rust-cargoCargo.lock 이 없어도 crate 를 잡는지
패키지 파일 없는 소스src-licenses, license-mix파일 헤더의 라이선스 검출
빌드 산출물
컨테이너 이미지container-image, image-debian-backport, image-log4shellOS 패키지 검출, 백포트 반영, 앱 계층 취약점
바이너리binary-busybox, binary-go파일 식별과 빌드 정보 읽기
펌웨어firmware-busybox펌웨어 파일시스템을 푸는지
AI
AI 모델 파일(형식 혼재, 위험 직렬화 포함)aibom-model-files모델 구성요소, 형식, 위험 직렬화 표시
취약점 판정
취약한 모듈을 쓰지만 영향 함수 호출 여부가 다름(Go)go-reach-called, go-reach-uncalled, go-reach-called-bin, go-reach-uncalled-bin호출 분석과 링크 분석으로 VEX not_affected 를 판정하는지