diff --git a/docker/lib/merge-sbom.sh b/docker/lib/merge-sbom.sh index 631d3d4..55b2c32 100755 --- a/docker/lib/merge-sbom.sh +++ b/docker/lib/merge-sbom.sh @@ -16,9 +16,10 @@ # input's root component name, or layer-), so provenance survives the # merge. Dedup keeps the first occurrence, so the first layer listed wins. # -# `dependencies` trees are NOT merged: bom-ref namespaces collide across inputs -# and the downstream pipeline only walks `.components[]`, so a merged tree would -# add risk without value. +# The per-layer `dependencies` graphs are merged too (edges unioned by ref), so +# the merged BOM keeps transitive-dependency information — required by the SKT +# conformance check. bom-refs rarely collide across ecosystems; identical refs +# have their dependsOn lists unioned. set -e OUTPUT="$1" @@ -65,6 +66,9 @@ for f in "$@"; do | select((.name // "") != "") | .properties = ((.properties // []) + [{name: "bomlens:layer", value: $L}]) ]' \ "$f" > "$WORK/comps-$i.json" + # Keep each layer's dependency graph so the merged BOM retains transitive + # edges (a mandatory SKT conformance check; dropping them fails it). + jq -c '[ .dependencies[]? ]' "$f" > "$WORK/deps-$i.json" valid=$((valid + 1)) i=$((i + 1)) done @@ -85,9 +89,23 @@ jq -s ' NTOTAL=$(jq 'length' "$WORK/merged.json") -# --slurpfile reads the merged components from a file (again, ARG_MAX safe). +# Merge the per-layer dependency graphs. Edges are preserved so the merged BOM +# keeps transitive-dependency information. bom-refs rarely collide across +# ecosystems (pkg:rpm vs pkg:npm …); when the same ref appears in more than one +# layer, its dependsOn lists are unioned. Entries with no edges are dropped. +jq -s ' + add + | group_by(.ref) + | map({ ref: .[0].ref, dependsOn: ([ .[].dependsOn[]? ] | unique) }) + | map(select((.ref != null) and ((.dependsOn | length) > 0))) +' "$WORK"/deps-*.json > "$WORK/deps.json" + +NEDGES=$(jq '[.[].dependsOn[]?] | length' "$WORK/deps.json") + +# --slurpfile reads the merged components/dependencies from files (ARG_MAX safe). jq -n \ --slurpfile comps "$WORK/merged.json" \ + --slurpfile deps "$WORK/deps.json" \ --arg name "$NAME" \ --arg version "$VERSION" \ --arg ts "$GEN_AT" ' @@ -100,7 +118,8 @@ jq -n \ tools: { components: [ { type: "application", name: "bomlens-merge" } ] }, component: { type: "application", name: $name, version: $version } }, - components: $comps[0] + components: $comps[0], + dependencies: $deps[0] }' > "$OUTPUT" -echo "[merge] SBOM written: $OUTPUT (components=${NTOTAL} from ${valid} layer(s))" +echo "[merge] SBOM written: $OUTPUT (components=${NTOTAL}, dependency edges=${NEDGES}, from ${valid} layer(s))" diff --git a/docs/guides/server-delivery.ko.md b/docs/guides/server-delivery.ko.md index aaa1de9..42ade73 100644 --- a/docs/guides/server-delivery.ko.md +++ b/docs/guides/server-delivery.ko.md @@ -87,7 +87,7 @@ done 각 층에서 두 값은 비슷해야 합니다. 차이가 크면 purl 없는 컴포넌트가 많다는 뜻이고, 보통 원시 디렉터리 스캔이나 수기 작성이 원인입니다. 그다음 [CycloneDX validator](https://github.com/CycloneDX/cyclonedx-cli)로 스키마 유효성을 확인합니다. -층을 분리해 두는 것이 기본인 이유가 있습니다. 검토자가 어느 층이 빠졌는지, 취약점이 어디 있는지 한눈에 보고, 각 SBOM이 자체 의존성 그래프(`dependencies`)를 그대로 유지하기 때문입니다. +층을 분리해 두는 것이 기본인 이유가 있습니다. 검토자가 어느 층이 빠졌는지, 취약점이 어디 있는지 한눈에 보고, 각 층의 의존성 그래프가 그 층 범위로 깔끔하게 유지되기 때문입니다. ## 선택: 단일 SBOM으로 병합 @@ -103,7 +103,7 @@ $SBOM --project mms-relay-server --version 1.0.0 \ 이 명령은 `mms-relay-server_1.0.0_bom.json`을 만들고, `metadata.component`를 서버 제품으로 설정하며, 병합된 컴포넌트 집합 위에 고지문과 위험분석보고서를 생성합니다. 각 컴포넌트에는 `bomlens:layer` 속성이 남으므로 층별로 걸러 볼 수 있습니다(`jq '.components[] | select(.properties[]?.value == "centos")'`). -한 가지 절충이 있습니다. 병합은 층별 `dependencies` 트리를 버립니다(`bom-ref` 네임스페이스가 충돌하기 때문). 전이 의존성 그래프가 검토에 중요하면 층을 분리해 제출하세요. +병합본은 각 층의 `dependencies` 그래프를 보존합니다(ref 기준으로 엣지를 합침). 따라서 전이 의존성 정보가 그대로 남아 SKT 적합성 검증의 전이 의존성 항목을 통과합니다. 생태계가 달라 `bom-ref` 충돌은 드물고, 같은 ref는 dependsOn 목록을 합칩니다. ## 서버 SBOM이 반려되는 경우 diff --git a/docs/guides/server-delivery.md b/docs/guides/server-delivery.md index a2a3435..cfa0cb0 100644 --- a/docs/guides/server-delivery.md +++ b/docs/guides/server-delivery.md @@ -87,7 +87,7 @@ done The two counts should be close for each layer. A large gap means many components lack a purl, which usually points to a raw-directory scan or a hand-written entry. Then validate the schema with the [CycloneDX validator](https://github.com/CycloneDX/cyclonedx-cli). -Keeping the layers separate is the default for a reason: a reviewer sees at a glance which layer is missing or where a vulnerability sits, and each SBOM keeps its own dependency graph (`dependencies`). +Keeping the layers separate is the default for a reason: a reviewer sees at a glance which layer is missing or where a vulnerability sits, and each layer's dependency graph stays cleanly scoped to that layer. ## Optional: merge into one SBOM @@ -103,7 +103,7 @@ $SBOM --project mms-relay-server --version 1.0.0 \ This writes `mms-relay-server_1.0.0_bom.json` with `metadata.component` set to the server product, plus the notice and risk report over the merged set. Each component keeps a `bomlens:layer` property, so you can still filter by layer (`jq '.components[] | select(.properties[]?.value == "centos")'`). -One trade-off: the merge drops the per-layer `dependencies` trees (their `bom-ref` namespaces collide). If the transitive-dependency graph matters for review, submit the layers separately instead. +The merge preserves each layer's `dependencies` graph (edges unioned by ref), so the merged BOM keeps its transitive-dependency information and passes the SKT conformance check. Cross-ecosystem `bom-ref` collisions are rare; identical refs have their dependsOn lists unioned. ## What gets a server SBOM rejected