본 글은 SBOM(Software Bill of Materials) 문서만으로 의존성 그래프의 깊이를 판정하는 Go CLI를 직접 구현하며 확인한 내용을 정리한 것입니다. 빌드 산출물 없이 문서 하나만 놓고 이 SBOM이 실제로 얼마나 깊은지를 판단할 수 있는가가 출발점이었습니다.

검증 환경은 Go 1.26.5이며, 파싱 라이브러리는 CycloneDX/cyclonedx-go v0.11.0과 spdx/tools-golang v0.5.7 기준입니다. 측정 표본은 실제 공개된 SBOM 51건입니다.
1. 전제 — 두 규제 모두 깊이를 요구하지 않습니다
SBOM에 무엇이 들어가야 하는지를 규정하는 문서는 크게 두 가지인데, 어느 쪽도 의존성 깊이를 강제하지 않습니다.
- EU 사이버복원력법(CRA)은 기계 판독 가능한 SBOM을 요구하면서 그 범위를 at the very least the top-level dependencies로 규정합니다(Regulation 2024/2847, Annex I). 최소선이며, 그 선이 낮습니다.
- 미국 CISA가 2026년 7월 공개한 2026 Minimum Elements는 전이 의존성을 포함한 전체 구성요소를 요구하지만, 같은 문단에서 There is no minimum depth라고 명시합니다.
한쪽은 한 단계 아래에 기준선을 긋고, 다른 한쪽은 기준선 자체를 긋지 않습니다. 결과적으로 공급업체는 직접 의존성만 나열한 문서를 제출하고도 규제상 문제가 없습니다. 그렇다면 그런 문서가 실제로 얼마나 흔한지, 그리고 문서만으로 그것을 판별할 수 있는지가 남습니다.
2. SPDX는 같은 관계를 양방향으로 표기합니다
SPDX에서 의존 관계는 DEPENDS_ON(A가 B에 의존)으로도, DEPENDENCY_OF(A는 B의 의존 대상, 즉 B가 A에 의존)으로도 표기할 수 있습니다. 둘 다 유효한 표기이며 생성 도구에 따라 갈립니다. GitHub가 자동 생성하는 SPDX 문서는 DEPENDS_ON을 쓰고, syft 계열이 만드는 릴리스 자산 SBOM은 거의 전부 DEPENDENCY_OF를 씁니다.
필자가 처음 작성한 측정 스크립트는 DEPENDS_ON만 처리했습니다. 그 결과 cosign의 릴리스 SBOM을 의존 관계 0건으로 보고했습니다. 실제로는 252건이 있습니다. 잘못된 쪽은 문서가 아니라 스크립트였습니다.
한 방향만 처리하는 도구는 반대 규약으로 생성된 모든 문서를 조용히 오판합니다. 에러도 경고도 없고, 단지 실제보다 비어 보이는 문서가 남습니다. 이것이 개인 코드에만 생기는 실수는 아닙니다. 확인 차원에서 널리 쓰이는 SBOM 품질 검사 도구를 직접 빌드해 대조했더니 같은 공백이 있었고, cosign의 관계 252건이 준수 항목에서 0점 처리됐습니다(interlynk-io/sbomqs#722로 보고).
3. 문서가 선언한 루트는 보장이 아니라 힌트입니다
SPDX 문서는 DESCRIBES 관계로 이 문서가 무엇에 관한 것인지를 선언할 수 있습니다. 그런데 cosign의 릴리스 SBOM은 DESCRIBES가 파일 엔트리(컴파일된 바이너리)를 가리키는 반면, 실제 의존성 그래프는 Go 모듈을 나타내는 패키지 엔트리에 뿌리를 두고 있습니다.
선언된 루트의 나가는 간선은 0개입니다. 거기서부터 순회하면 아무것도 얻지 못하지만, 실제 의존성 데이터는 세 홉 떨어진 곳에 그대로 존재합니다.
해결 방식은 단순합니다. 선언된 루트가 동작하지 않으면 들어오는 간선이 없는 노드로 폴백하되, 그 사실을 출력에 명시하도록 합니다(rootSource를 declared가 아니라 in-degree-zero로 표기). 선언이 틀렸다고 문서를 통째로 버리면 실제 정보를 버리는 것이고, 추론한 루트를 선언된 것처럼 보고하면 그것대로 사실과 다릅니다.
4. NOASSERTION을 노드로 취급하면 빈 문서가 통과합니다
SPDX는 관계의 대상으로 NONE과 NOASSERTION을 허용합니다. 둘 다 확정되지 않음을 뜻하며, NONE이라는 이름의 컴포넌트를 가리킨다는 의미가 아닙니다. 초기 관계 파서는 이 값을 특수 처리하지 않고 유효한 요소 참조로 취급했습니다.
결과는 다음과 같습니다. 실제 의존성을 하나도 선언하지 않았지만 모든 DEPENDS_ON을 NOASSERTION 센티널로 향하게 한 문서가 완벽하게 연결된 그래프로 보입니다. 모든 간선이 같은 유령 노드로 해석되고 순회기가 그 노드를 도달 가능으로 처리하기 때문입니다. 쓸 만한 의존성 정보가 전혀 없는 문서가 CRA 최소 요건을 충족한 것으로 판정됩니다.
이런 부류의 결함은 찾으러 가기 전까지 보이지 않습니다. 크래시가 나지도 않고 결과가 이상해 보이지도 않으며, 그럴듯하고 밋밋한 FLAT, 최소선 충족 판정을 내놓습니다. 이 건은 맥락 없이 투입된 코드 리뷰어가 정확히 그런 문서를 합성해 빌드된 바이너리에 통과시켜 잡아냈습니다. 포맷이 무엇을 허용하는지만 생각했고, 할 말이 없는 문서가 파서를 통과하면 어떻게 보이는지는 생각하지 못한 탓입니다.
5. 필드가 채워져 있어도 정보가 없을 수 있습니다
이후 CISA의 17개 최소 데이터 필드 검사를 추가하면서 각 필드를 담고 있는 컴포넌트 수를 세게 됐습니다. Component Producer는 쉬워 보였습니다. SPDX에 supplier 필드가 있으니 그것을 설정한 패키지를 세면 됩니다.
cosign 릴리스 SBOM의 패키지 254개가 전부 이 필드를 설정하고 있었습니다. 그리고 254개 전부가 NOASSERTION이었습니다.
NOASSERTION은 SPDX가 확정되지 않음을 표현하는 방식이고, 이렇게 쓰는 것이 올바른 동작입니다. CISA도 정보가 불명확할 때 빈칸으로 두지 말고 그 사실을 명시하라고 요구합니다. 생산자는 규격대로 한 것입니다. 다만 필드 존재 여부만 세는 도구는 아무 정보도 전달하지 않는 필드에 대해 모든 컴포넌트가 이를 갖췄다고 보고하고, 그 숫자를 읽은 조달 담당자는 사실과 반대되는 결론을 얻습니다.
중요한 구분은 존재하느냐 아니냐가 아니라, 그 안에 정보가 있느냐입니다.
6. 숫자는 맞았고 분모가 틀렸습니다
같은 검사가 cosign의 Component Version 커버리지를 255개 중 254개, 즉 부분 충족으로 보고했습니다. 원본 JSON과 대조해 확인한 결과 254개 패키지에 버전이 있고 문서 엔트리는 255개였습니다. 산술은 정확했습니다.
문제는 255번째 엔트리가 패키지가 아니라 파일이라는 점입니다. SPDX는 파일을 별도 섹션에 별도 스키마로 나열하며, 그 스키마에는 버전 필드도, originator도, 외부 참조도 없습니다. 이 문서는 한 건이 빠진 부분 충족이 아니라 완전 충족이었고, 도구가 해당 필드를 결코 충족할 수 없는 엔트리를 분모에 포함시킨 것입니다. 그 결과 출력에는 이 항목에 버전을 추가하도록 공급업체에 요청하라는 문구가, 버전을 가질 수 없는 대상에 대해 표시됐습니다.
이 결함은 숫자가 맞았기 때문에 검증 단계를 통과했습니다. 출력을 원본 데이터와 대조하는 검증은 산술만 잡아냅니다. 잘못된 모집단을 셌다는 사실은 알려주지 못합니다.
7. 라이브러리가 서명을 버리고, 그다음 죽습니다
CISA 2026 업데이트는 SBOM 작성자 서명을 최소 요소에 포함시켰습니다. 그래서 해당 CycloneDX 문서에 서명이 있는지를 검사하도록 연결했습니다. 답은 언제나 없음이었습니다.
사용 중인 Go 라이브러리는 서명 블록을 모델링하면서 단일 서명자 필드를 임베디드 포인터로 두고 json:"-" 태그를 붙여 놓았습니다. 그 결과 스펙 예제가 사용하는 인라인 형식이 디코딩 과정에서 통째로 버려집니다. 서명된 모든 문서가 미서명으로 파싱됩니다.
// cyclonedx-go: cyclonedx.go
type JSFSignature struct {
*JSFSigner `json:"-" xml:"-"`
Signers *[]JSFSigner `json:"signers,omitempty" xml:"-"`
Chain *[]JSFSigner `json:"chain,omitempty" xml:"-"`
}
signers 배열로 서명한 문서에서 같은 필드에 접근하면 상황이 더 나쁩니다. 그 형태는 임베디드 구조체를 nil로 남기므로 필드 접근이 panic으로 이어집니다. 유효하고 정상적으로 서명된 SBOM이 도구를 중단시킵니다.
결국 파싱된 모델 대신 원본 바이트에서 서명을 직접 읽는 방식으로 우회했습니다. 처음에는 편법처럼 느껴졌지만, 같은 코드베이스가 스펙 버전에 대해 이미 같은 처리를 하고 있었고 이유도 동일했습니다. 라이브러리는 파싱한 것을 재작성하며, 공급업체 문서에 대한 리포트는 라이브러리가 이해한 그림이 아니라 문서 자체를 서술해야 합니다. 다른 프로젝트 소스에서 같은 우회와 그 이유를 설명한 주석을 발견하고 나서는 편법이라는 느낌이 사라졌습니다. 이 건은 상류에 보고했습니다(CycloneDX/cyclonedx-go#280).
8. 준수와 유용성은 다른 축입니다
규제 문서를 정확히 읽으면 이 선은 스스로 그어집니다. CRA의 최소선은 최상위 의존성이고, CISA는 깊이 요구를 두지 않으며, CISA 자신이 최소 요소가 데이터의 정확성·범위·완전성에 대해 아무것도 보증하지 않는다고 명시합니다. 직접 의존성만 나열한 문서는 완전히 준수 상태이면서, 깊은 전이 취약점이 제품에 도달하는지 추적하는 용도로는 거의 쓸모가 없습니다.
그래서 이 도구는 준수와 유용성을 하나의 점수로 평균 내지 않고 별개로 보고하도록 설계했습니다. 준수 출력이 매 실행마다 한계 문구를 함께 내보내는 이유도 같습니다. 문서는 17개 필드를 전부 채우고도 컴포넌트의 절반을 누락할 수 있습니다.
같은 논리에서 계획에 없던 네 번째 판정 상태가 나왔습니다. SPDX 2.3은 CISA 17개 필드 중 3개를 표현할 수단 자체가 없습니다. 서명 필드도, 라이프사이클 단계도, 문서 버전도 없습니다. 이것을 누락으로 보고하면 공급업체에게 포맷이 할 수 없는 일을 고치라고 요구하는 셈입니다. 그래서 unrepresentable로 따로 표시하고 요청 항목 목록에서 제외합니다.
9. 측정 결과 — 직접 측정한 SBOM 51건 기준 45%
Go, JavaScript, Python, PHP, Rust 생태계의 GitHub 자동 생성 문서와 벤더 공개 릴리스 자산을 섞은 실제 SBOM 51건을 측정했습니다. 이 수치는 인용이 아니라 아래 저장소의 측정 스크립트로 직접 산출한 결과 기준입니다. 그중 23건(45%)이 직접 의존성 이상의 정보를 갖고 있지 않았습니다. 손상되거나 형식이 잘못된 문서가 아니라, 완전히 유효하고 CRA 최소선을 충족하지만 그 아래에 무엇이 있는지는 말하지 않는 문서입니다.
이는 문서 단위 측정이지 설문 응답이 아니므로, 조직의 45%가 수신하는 SBOM의 깊이를 알지 못한다는 ENISA 2026 조사 결과와 직접 비교할 수는 없습니다. 질문도 방법도 다르고 숫자가 일치한 것은 우연일 가능성이 큽니다. 두 결과를 함께 놓았을 때 근본 문제가 흔하다는 판단에 무게가 실렸다는 의미로만 언급합니다.
같은 51건에서 하나 더 확인한 사실이 있습니다. GitHub 자동 생성 SBOM에는 체크섬이 전혀 없습니다. 적은 것이 아니라 0건이며, spring-boot 문서의 컴포넌트 303개 전부에 해당합니다. CISA 2026 업데이트는 컴포넌트 해시 값과 해시 알고리즘을 최소 요소로 지정했으므로, 대부분의 저장소가 API 호출 한 번으로 생성할 수 있는 SBOM이 구조적으로 새 기준선을 충족하지 못한다는 뜻이 됩니다. 누군가 결함을 넣은 것이 아니라, 생성기가 요구사항보다 먼저 만들어졌을 때 생기는 자연스러운 결과입니다.
10. 정리
룰셋(CISA 2026, NTIA, 그 이후에 나올 것들)은 쉬운 부분입니다. 활발히 유지되는 SBOM 도구라면 언젠가 추가하게 됩니다. 도구를 하나 만들면서 배운 것은 준수 규격보다 파싱에 관한 쪽이 많았습니다. 위 일곱 가지 중 다섯 가지가 문서는 한 가지를 말하는데 코드가 다른 것을 믿은 경우입니다.
- 포맷이 허용하는 표기가 둘이면, 둘 다 처리하거나 파싱 단계에서 하나로 정규화하도록 합니다.
- 문서의 자기 선언(루트, 필드 존재)은 힌트로 취급하고, 폴백을 사용한 사실은 출력에 남깁니다.
- 센티널 값(
NOASSERTION,NONE)은 노드가 아닙니다. 그래프에 넣는 순간 빈 문서가 통과합니다. - 커버리지를 셀 때는 분자보다 분모를 먼저 검증합니다. 산술 대조로는 잡히지 않습니다.
- 라이브러리 모델이 원본을 손실 없이 표현한다고 가정하지 않습니다.
더 어렵고 흥미로운 질문은 CISA가 명시적으로 미룬 쪽입니다. SBOM의 인벤토리가 실제로 완전한가는 문서가 아니라 빌드 산출물이 있어야 답할 수 있습니다. 현재 범위 밖이며, 여기까지가 문서만으로 도달한 지점입니다.
구현체와 측정 스크립트는 github.com/rexx4314/sbomdepth에 공개돼 있습니다.
📌 함께 보시면 좋은 글
이직·퇴사·연봉 협상을 혼자 판단하기 어렵다면
1:1 개발자 커리어 상담 신청하기 →
※ 본 글은 AI(Claude)의 초안을 기반으로 편집자 검수를 거쳐 발행되었습니다. (한국 AI기본법 대응 고지)
이직·퇴사, 지금 움직여도 될지 헷갈리시나요?
막연히 불안한 건지, 정말 시점이 온 건지 판단이 어려울 때가 있습니다.
5분 체크리스트로 지금 상태를 먼저 정리해보세요.
결론을 대신 내리기보다, 스스로 판단할 기준을 잡는 데 도움을 드립니다.
아직 확신이 없다면, 지금이 ‘고민 단계’인지부터 먼저 점검해보세요