분석
리더보드를 읽는 데 필요한 규칙과, 숫자를 해석하는 근거를 모았다.
지표와 채점 규칙
지표
도구가 낸 결과를 사람이 큐레이션한 정답과 대조해 잰다. 정확도 지표 다섯 가지와, 이번 회차에 생긴 도달성과 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 는 이미지 입력을 지원하지 않아 이 카테고리에서 빠진다
더 보기
입력 유형별 참조
| 입력 | 검증 대상 | 변별점 |
|---|---|---|
| 소스코드 | ||
| 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-pinned | lockfile 이 있으면 도구가 수렴하고, 없으면 설치 해석에서 갈린다 |
| Node.js (pnpm 워크스페이스, Bun) | pnpm-workspace, bun-lock | 새 lockfile 형식을 읽는지, 개발 의존성을 빼는지 |
| 예제, 테스트 디렉터리가 섞인 저장소 | npm-nonshipped | 배포되지 않는 매니페스트와 개발 의존성을 빼는지 |
| Python (requirements.txt) | py-pinned, py-range | 간접 의존성과 버전 범위 해석 |
| Python (uv.lock) | uv-lock | uv lockfile 을 읽는지, 개발 의존성 그룹을 빼는지 |
| PHP, Ruby (lockfile 없음) | php-nolock, ruby-nolock | lockfile 없이 의존성을 해석하는지, 개발 의존성을 빼는지 |
| Go (go.mod) | go-mod | 실제 링크와 선언 전체의 차이 |
| Rust (Cargo.toml) | rust-cargo | Cargo.lock 이 없어도 crate 를 잡는지 |
| 패키지 파일 없는 소스 | src-licenses, license-mix | 파일 헤더의 라이선스 검출 |
| 빌드 산출물 | ||
| 컨테이너 이미지 | container-image, image-debian-backport, image-log4shell | OS 패키지 검출, 백포트 반영, 앱 계층 취약점 |
| 바이너리 | 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 를 판정하는지 |