빌드는 초록불이었는데 그 라이브러리는 누가 넣었나 · 직접 고른 건 일곱 개, 설치된 건 스물다섯 개 · 이론
직접 고른 건 일곱 개, 설치된 건 스물다섯 개
한 줄 요약
의존성 목록은 우리가 적은 파일(package.json)이 아니라 실제로 설치된 것을 적어 둔
파일(package-lock.json)에 있고, 그 둘의 개수 차이가 공급망 보안이 어려운 이유의
거의 전부입니다.
왜 이게 필요했나
사고가 터진 다음 날 아침에 가장 먼저 오는 질문은 언제나 같습니다. "우리 그거 쓰나요."
이 질문이 어려운 이유는 답이 복잡해서가 아니라, 답할 자료가 어디에도 없어서입니다.
저장소에는 우리가 손으로 적은 의존 목록이 있습니다. 그런데 거기 적힌 이름들은 각자
또 다른 이름들을 끌고 오고, 그 이름들이 다시 다른 이름들을 끌고 옵니다. 다섯 줄이
스물다섯 개가 되는 데는 아무 사건도 필요 없습니다. 그냥 설치하면 그렇게 됩니다.
그래서 "무엇이 들어 있는가" 는 빌드가 끝난 뒤에 다시 물어야 하는 질문이 됩니다.
소스를 아무리 읽어도 답이 안 나옵니다. 답은 빌드 시점에 결정되고, 그 순간을 지나가면
사라집니다. 자물쇠 파일(lockfile)과 SBOM 은 그 순간을 붙잡아 두려고 만들어진 것입니다.
한 가지를 더 갈라야 합니다. 설치된 것이 전부 배포에 실리지는 않습니다. 시험 도구,
번들러, 린터는 빌드 기계에서만 돌고 사용자에게 닿지 않습니다. 이 둘을 한 칸에 세면
"고쳐야 할 것" 의 숫자가 두 배로 부풀고, 부푼 숫자는 아무도 안 봅니다.
어떻게 동작하나
자물쇠 파일은 "무엇을 받았는지" 를 못박습니다. npm 의 package-lock.json 은lockfileVersion 3 에서 packages 라는 맵을 쓰는데, **설치 위치를 열쇠로 삼고 루트
프로젝트는 빈 문자열 열쇠로 들어갑니다.** 각 항목에는 version, 받아 온 자리를 적은resolved, 그리고 받은 바이트가 그것이 맞는지를 보증하는 integrity 가 있습니다.
개발 전용 트리에만 속하는 항목에는 dev 가 true 로 붙습니다
([npm 문서](https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json)).
개수를 셀 때 루트 항목을 빼지 않아 하나가 더 나오는 실수가 여기서 가장 흔합니다.
SBOM 은 그 목록을 도구와 조직을 건너다닐 수 있는 형식으로 옮긴 것입니다. 널리
쓰이는 두 형식이 있습니다. CycloneDX 는 OWASP 와 Ecma 가 함께 관리하고 ECMA-424 로
표준화돼 있으며, 구성 요소와 서비스, 그리고 **직접 의존과 전이 의존을 모두 담는
의존 그래프**를 표현합니다
([CycloneDX 개요](https://cyclonedx.org/specification/overview/)). JSON 문서의
첫 두 칸은 bomFormat 과 specVersion 인데, bomFormat 값이 "CycloneDX" 하나로
못박혀 있는 이유가 재미있습니다 — BOM 파일에는 이름 규칙이 없고 JSON 스키마에는
네임스페이스가 없어서, 파일만 보고도 이게 무슨 형식인지 알 수 있어야 하기
때문입니다([CycloneDX 1.6 JSON](https://cyclonedx.org/docs/1.6/json/)).
SPDX 는 같은 일을 다른 어휘로 합니다. 꾸러미마다 이름과 버전, 체크섬, 그리고purl 같은 외부 참조를 적고, 요소 사이의 관계를 DEPENDS_ON·CONTAINS 같은
관계 이름으로 표현합니다
([SPDX 2.3 Package Information](https://spdx.github.io/spdx-spec/v2.3/package-information/)).
어느 쪽을 쓰든 실제로 중요한 칸은 셋입니다. **대상이 무엇인지, 무엇이 들어 있는지,
그리고 무엇이 무엇을 끌어왔는지.** 마지막 칸이 없는 SBOM 은 평평한 목록일 뿐이라
"이건 왜 여기 있죠" 에 답하지 못합니다.
현장에서 만나는 모습
가장 자주 보는 장면은 SBOM 은 만드는데 아무도 두 개를 견주지 않는 것입니다.
릴리스마다 파일이 쌓이지만 지난 릴리스와 이번 릴리스의 차이를 보는 사람이 없습니다.
그런데 공급망 사고의 신호는 대부분 그 차이에 있습니다 — 아무도 의존을 추가하지
않았는데 목록에 새 이름이 하나 늘어나 있는 것. 누군가 간접 의존의 버전 범위를
넓게 잡아 두었고, 새 판이 나오면서 딸려 들어온 것입니다.
두 번째 장면은 스캔 결과가 부풀어 무시되는 것입니다. 개발 전용 의존까지 함께 세면
숫자가 두 배가 되고, 두 배가 된 목록은 개발팀에서 읽히지 않습니다. 나눠서 보내야
읽힙니다.
세 번째 장면은 목록의 대상이 적혀 있지 않은 것입니다. components 는 스물다섯 줄이
빼곡한데 이 목록이 어느 산출물의 것인지가 어디에도 없습니다. 그러면 몇 달 뒤 그 파일을
열어 본 사람은 "이게 그때 그 릴리스 맞나" 를 확인할 방법이 없습니다. 대상은 이름이 아니라
다이제스트로 적어야 합니다 — 이름은 같은 이름으로 다른 내용을 가리킬 수 있지만 해시는
그럴 수 없습니다. 이어지는 모듈의 서명과 출처 증명이 전부 이 '대상' 칸 위에 얹힙니다.
마지막으로, 목록을 언제 만드느냐도 설계입니다. 소스를 읽어서 만든 목록과 빌드 산출물을
훑어서 만든 목록은 다릅니다. 앞의 것은 "무엇을 쓰기로 했나" 이고 뒤의 것은 "무엇이 실제로
실려 나갔나" 입니다. 둘은 대개 비슷하지만, 어긋나는 날이 바로 사고가 나는 날입니다.
다음 실습에서 할 것
잠금 파일 하나에서 목록을 직접 세어 CycloneDX 형식으로 적어 봅니다. 아무도 고르지
않은 패키지가 어느 경로로 들어왔는지 추적하고, 지난 릴리스의 SBOM 과 견주어 무엇이
늘고 줄고 바뀌었는지까지 세어 봅니다. 도구가 1초에 하는 일을 손으로 한 번 해 두면,
나중에 도구가 내놓은 목록에서 무엇이 빠졌는지 보입니다.