Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package apko for openSUSE:Factory checked in at 2026-10-01 16:46:38 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/apko (Old) and /work/SRC/openSUSE:Factory/.apko.new.1253 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "apko" Thu Oct 1 16:46:38 2026 rev:139 rq:1381771 version:1.4.6 Changes: -------- --- /work/SRC/openSUSE:Factory/apko/apko.changes 2026-09-28 10:48:47.958851662 +0200 +++ /work/SRC/openSUSE:Factory/.apko.new.1253/apko.changes 2026-10-01 16:47:29.053145110 +0200 @@ -1,0 +2,15 @@ +Thu Oct 01 04:59:24 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.4.6: + * build(deps): bump google.golang.org/api from 0.298.0 to 0.299.0 + (#2524) + * fix: pin libssl3 in old-glibc example (#2527) + * build(deps): bump k8s.io/apimachinery from 0.37.0 to 0.37.1 + (#2530) + * build(deps): bump github.com/klauspost/compress from 1.20.0 to + 1.20.1 (#2531) + * build(deps): bump chainguard.dev/sdk from 0.1.278 to 0.1.293 + (#2537) + * build(deps): bump the codeql group with 2 updates (#2533) + +------------------------------------------------------------------- Old: ---- apko-1.4.5.obscpio New: ---- apko-1.4.6.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ apko.spec ++++++ --- /var/tmp/diff_new_pack.Jc31Qi/_old 2026-10-01 16:47:30.740215820 +0200 +++ /var/tmp/diff_new_pack.Jc31Qi/_new 2026-10-01 16:47:30.742215903 +0200 @@ -17,7 +17,7 @@ Name: apko -Version: 1.4.5 +Version: 1.4.6 Release: 0 Summary: Build OCI images from APK packages directly without Dockerfile License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.Jc31Qi/_old 2026-10-01 16:47:30.788217831 +0200 +++ /var/tmp/diff_new_pack.Jc31Qi/_new 2026-10-01 16:47:30.792217999 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/chainguard-dev/apko.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v1.4.5</param> + <param name="revision">refs/tags/v1.4.6</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.Jc31Qi/_old 2026-10-01 16:47:30.829219550 +0200 +++ /var/tmp/diff_new_pack.Jc31Qi/_new 2026-10-01 16:47:30.834219759 +0200 @@ -3,6 +3,6 @@ <param name="url">https://github.com/chainguard-dev/apko</param> <param name="changesrevision">861f83f69e6fa9114405a2f7bb5cf6585ad00421</param></service><service name="tar_scm"> <param name="url">https://github.com/chainguard-dev/apko.git</param> - <param name="changesrevision">739e7ce3f675ffb232e16849b51ff93657f1570c</param></service></servicedata> + <param name="changesrevision">51c298aa5249f43193e3cb8684a2a60facfcb69d</param></service></servicedata> (No newline at EOF) ++++++ apko-1.4.5.obscpio -> apko-1.4.6.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/examples/old-glibc.yaml new/apko-1.4.6/examples/old-glibc.yaml --- old/apko-1.4.5/examples/old-glibc.yaml 2026-09-22 00:49:38.000000000 +0200 +++ new/apko-1.4.6/examples/old-glibc.yaml 2026-09-30 14:19:13.000000000 +0200 @@ -11,6 +11,7 @@ - openssl=3.0.7-r0 - sysstat=12.6.2-r0 - libcrypto3=3.0.8-r0 + - libssl3=3.6.4-r7 archs: - x86_64 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/go.mod new/apko-1.4.6/go.mod --- old/apko-1.4.5/go.mod 2026-09-22 00:49:38.000000000 +0200 +++ new/apko-1.4.6/go.mod 2026-09-30 14:19:13.000000000 +0200 @@ -3,10 +3,11 @@ go 1.27.0 require ( - chainguard.dev/sdk v0.1.278 + chainguard.dev/sdk v0.1.293 github.com/chainguard-dev/clog v1.8.1 github.com/charmbracelet/log v1.0.0 github.com/erofs/go-erofs v0.3.2-0.20260804074615-52cc42c5291c + github.com/github/go-spdx/v2 v2.7.0 github.com/go-git/go-git/v5 v5.19.2 github.com/google/go-cmp v0.7.0 github.com/google/go-containerregistry v0.22.1 @@ -15,7 +16,7 @@ github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/invopop/jsonschema v0.14.0 - github.com/klauspost/compress v1.20.0 + github.com/klauspost/compress v1.20.1 github.com/klauspost/pgzip v1.2.6 github.com/package-url/packageurl-go v0.1.7 github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 @@ -34,18 +35,18 @@ golang.org/x/sys v0.48.0 golang.org/x/term v0.46.0 golang.org/x/time v0.16.0 - google.golang.org/api v0.298.0 + google.golang.org/api v0.299.0 gopkg.in/ini.v1 v1.67.3 gopkg.in/yaml.v3 v3.0.1 - k8s.io/apimachinery v0.37.0 + k8s.io/apimachinery v0.37.1 sigs.k8s.io/release-utils v0.12.4 ) require ( chainguard.dev/go-grpc-kit v0.20.0 // indirect - cloud.google.com/go/auth v0.23.2 // indirect + cloud.google.com/go/auth v0.23.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/compute/metadata v0.9.1 // indirect dario.cat/mergo v1.0.2 // indirect filippo.io/edwards25519 v1.2.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -83,9 +84,9 @@ github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/s2a-go v0.1.9 // indirect + github.com/google/s2a-go v0.1.10 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.20 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.22 // indirect github.com/googleapis/gax-go/v2 v2.24.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.4 // indirect @@ -135,8 +136,8 @@ golang.org/x/net v0.59.0 // indirect golang.org/x/text v0.42.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect - google.golang.org/grpc v1.83.2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 // indirect + google.golang.org/grpc v1.84.0 // indirect google.golang.org/protobuf v1.36.12 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/go.sum new/apko-1.4.6/go.sum --- old/apko-1.4.5/go.sum 2026-09-22 00:49:38.000000000 +0200 +++ new/apko-1.4.6/go.sum 2026-09-30 14:19:13.000000000 +0200 @@ -1,13 +1,13 @@ chainguard.dev/go-grpc-kit v0.20.0 h1:MDwZtTlUlMSMEPcoi9m8tnu4g+M88T1toUZ3Th/+0Gc= chainguard.dev/go-grpc-kit v0.20.0/go.mod h1:ocuwyRX9tqRRvJkS3/ZkDYGMnl+6FLC4bvF0cm6DGsk= -chainguard.dev/sdk v0.1.278 h1:vM5AkOAlrhMXpEQumITidz+w0nQxNIYAXbyPN5nqD6o= -chainguard.dev/sdk v0.1.278/go.mod h1:qdToj7HJ0neJdut3yLmYC1TIfF6sp5RPmlFXgZ4r1kA= -cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo= -cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0= +chainguard.dev/sdk v0.1.293 h1:g2cSwqFulEqLYN6jB9pRkVdXoLttq1CHx+LgmMRRGcU= +chainguard.dev/sdk v0.1.293/go.mod h1:fDIxqEbOn4iyGhS1ggkdcbrePUTBCJljMzH2XlTWwS4= +cloud.google.com/go/auth v0.23.3 h1:UMK+oBtuNGMCR/6i6mmySUItqjOazpJrbmZyhGbGBWo= +cloud.google.com/go/auth v0.23.3/go.mod h1:fClbry28fo7XkxhSeT6AQtAVAp6Jy0fW9N99PoPNPFM= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/compute/metadata v0.9.1 h1:CTE1OWBQ0vnF5uHwdFAQJvMQ0Fi/KRcqqKTo9V0F8Ik= +cloud.google.com/go/compute/metadata v0.9.1/go.mod h1:NtnlvB6X3t4R6xSWyVX/ZWk493PCxGQlhI/iqxh4M8I= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= @@ -86,6 +86,8 @@ github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XFdQ= +github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -115,14 +117,14 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84= github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/s2a-go v0.1.10 h1:EMp+aOuXN6l8cE/gjF5Bt+vyZxsUuyCWe9chDWR/+uU= +github.com/google/s2a-go v0.1.10/go.mod h1:pz4tyvwXvJLLbyrkh6FW1eS2zPUXMaTmyNhYtyP2tNw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.20 h1:t/xL64VUoN69MuMRQuJETqYGOw4Z9mSRJK9epIEtwFk= -github.com/googleapis/enterprise-certificate-proxy v0.3.20/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= +github.com/googleapis/enterprise-certificate-proxy v0.3.22 h1:NU4XpII6jD+Dxcot94fqjE+AfJoE/lQP9q3faYGzC/c= +github.com/googleapis/enterprise-certificate-proxy v0.3.22/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= github.com/googleapis/gax-go/v2 v2.24.1 h1:AtqTN21IXMMWo99LiEVAiBfNNQmO40d8xUfZI640mc0= github.com/googleapis/gax-go/v2 v2.24.1/go.mod h1:bWeBei0NVwaNZKb2y1HUBS7gLXIF3/Tu3pq7j8D2Tb0= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= @@ -149,8 +151,8 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= -github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= @@ -356,16 +358,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.298.0 h1:YW18RkHBMZBA1ergX0m4biagzgbiPTb2uTsRsDPWNRY= -google.golang.org/api v0.298.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4= +google.golang.org/api v0.299.0 h1:b3K+ydSMd0kh6TQI6bJyApRQfqQX2MfSOaVkpM59mJw= +google.golang.org/api v0.299.0/go.mod h1:zlR3GVA8b2R5nv5Ij9UWe37StVB3cxDD7DBFi4ZFsHw= google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms= google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459 h1:b0xCahf3FK2m2Cv0p4vTozGPWncCvLfwV86UNg8xWU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260921155816-b14227669459/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= +google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= +google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -383,8 +385,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0= -k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE= +k8s.io/apimachinery v0.37.1 h1:hGCYyvKHCwtwMitj2vU4vYx0Z16N9GyZk9BBnz0wDAE= +k8s.io/apimachinery v0.37.1/go.mod h1:jF84AyUi/IRIXRot5f+lm6MpxoWI+F1XgjaMmwCdTFw= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/release-utils v0.12.4 h1:kuG6WTWGCKx5uUrJwl2uFErOKOw+4Ba8WrPmOQh5J3g= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/pkg/apk/expandapk/expandapk.go new/apko-1.4.6/pkg/apk/expandapk/expandapk.go --- old/apko-1.4.5/pkg/apk/expandapk/expandapk.go 2026-09-22 00:49:38.000000000 +0200 +++ new/apko-1.4.6/pkg/apk/expandapk/expandapk.go 2026-09-30 14:19:13.000000000 +0200 @@ -31,7 +31,7 @@ ) var readerPool = sync.Pool{ - New: func() interface{} { + New: func() any { return bufio.NewReaderSize(nil, 1<<20) }, } @@ -43,7 +43,7 @@ } var writerPool = sync.Pool{ - New: func() interface{} { + New: func() any { return bufio.NewWriterSize(nil, 1<<20) }, } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/internal_sbom_test.go new/apko-1.4.6/pkg/sbom/generator/spdx/internal_sbom_test.go --- old/apko-1.4.5/pkg/sbom/generator/spdx/internal_sbom_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.4.6/pkg/sbom/generator/spdx/internal_sbom_test.go 2026-09-30 14:19:13.000000000 +0200 @@ -0,0 +1,358 @@ +// Copyright 2026 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spdx + +import ( + "archive/tar" + "encoding/json" + "io/fs" + "os" + "path/filepath" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "chainguard.dev/apko/pkg/apk/apk" + apkfs "chainguard.dev/apko/pkg/apk/fs" +) + +// installed returns an installed package whose database entry lists owns. +func installed(name, version string, owns ...string) *apk.InstalledPackage { + ipkg := &apk.InstalledPackage{Name: name, Version: version, Arch: "x86_64", License: "MIT"} + for _, p := range owns { + ipkg.Files = append(ipkg.Files, tar.Header{Name: p}) + } + return ipkg +} + +func sbomAt(nameVersion string) string { + return "var/lib/db/sbom/" + nameVersion + ".spdx.json" +} + +func apkRef(name, version string) ExternalRef { + return ExternalRef{ + Category: "PACKAGE_MANAGER", + Type: "purl", + Locator: "pkg:apk/wolfi/" + name + "@" + version + "?arch=x86_64", + } +} + +func record(name, version string, refs ...ExternalRef) Package { + return Package{ + ID: stringToIdentifier("SPDXRef-Package-" + name + "-" + version), + Name: name, + Version: version, + ExternalRefs: refs, + } +} + +// fixture reads a real apk SBOM from testdata. +func fixture(t *testing.T, name string) []byte { + t.Helper() + b, err := os.ReadFile(filepath.Join("testdata", "apk_sboms", name)) + require.NoError(t, err) + return b +} + +// internalSBOM encodes a document that describes primary and reaches each of reachable from it. +func internalSBOM(t *testing.T, primary Package, reachable ...Package) []byte { + t.Helper() + doc := Document{ + ID: "SPDXRef-DOCUMENT", + DocumentDescribes: []string{primary.ID}, + Packages: append([]Package{primary}, reachable...), + } + for _, r := range reachable { + doc.Relationships = append(doc.Relationships, Relationship{ + Element: primary.ID, Type: "GENERATED_FROM", Related: r.ID, + }) + } + b, err := json.Marshal(doc) + require.NoError(t, err) + return b +} + +func TestInternalSBOMIdentity(t *testing.T) { + source := Package{ + ID: "SPDXRef-Package-github.com-example-foo-v1.0.0", + Name: "foo-src", + Version: "v1.0.0", + ExternalRefs: []ExternalRef{{ + Category: "PACKAGE_MANAGER", Type: "purl", Locator: "pkg:github/example/[email protected]", + }, { + Category: "SECURITY", Type: "cpe23Type", Locator: "cpe:2.3:a:example:foo:1.0.0:*:*:*:*:*:*:*", + }}, + Checksums: []Checksum{{Algorithm: "SHA1", Value: "a1"}, {Algorithm: "SHA256", Value: "b2"}}, + } + // Builds of one upstream source can disagree on its license or reference order. + relicensed := source + relicensed.LicenseDeclared = "BSD-3-Clause" + reordered := source + reordered.ExternalRefs = slices.Clone(source.ExternalRefs) + slices.Reverse(reordered.ExternalRefs) + reordered.Checksums = slices.Clone(source.Checksums) + slices.Reverse(reordered.Checksums) + repurled := source + repurled.ExternalRefs = []ExternalRef{{ + Category: "PACKAGE_MANAGER", Type: "purl", Locator: "pkg:github/example/[email protected]", + }} + rehashed := source + rehashed.Checksums = []Checksum{{Algorithm: "SHA256", Value: "c3"}} + foo := record("foo", "1.0.0-r0", apkRef("foo", "1.0.0-r0")) + openssl := record("openssl", "3.0.1-r0", apkRef("openssl", "3.0.1-r0")) + + for _, tt := range []struct { + name string + pkgs []*apk.InstalledPackage + files map[string][]byte + present []string // name@version entries the image SBOM must hold + absent []string // name@version entries it must not hold + purls []string // PURLs it must hold + wantErr string + }{{ + name: "matching SBOM is copied with its reachable packages", + pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))}, + files: map[string][]byte{sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source)}, + present: []string{"[email protected]", "[email protected]"}, + }, { + name: "SBOM version without the epoch is accepted", + pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))}, + files: map[string][]byte{sbomAt("foo-1.0.0-r0"): internalSBOM(t, record("foo", "1.0.0"))}, + present: []string{"[email protected]"}, + }, { + name: "SBOM cataloging the package's own apk entry is accepted", + pkgs: []*apk.InstalledPackage{ + installed("wolfi-baselayout", "20230201-r30", sbomAt("wolfi-baselayout-20230201-r30")), + }, + files: map[string][]byte{ + sbomAt("wolfi-baselayout-20230201-r30"): fixture(t, "wolfi-baselayout-20230201-r30.spdx.json"), + }, + present: []string{"wolfi-baselayout@20230201-r30"}, + purls: []string{ + "pkg:apk/wolfi/wolfi-baselayout@20230201-r30?arch=x86_64&origin=wolfi-baselayout", + }, + }, { + name: "package without an SBOM is described from the installed database", + pkgs: []*apk.InstalledPackage{installed("no-sbom", "1.0.0-r0", "usr/bin/no-sbom")}, + present: []string{"[email protected]"}, + purls: []string{"pkg:apk/unknown/[email protected]?arch=x86_64"}, + }, { + name: "SBOM owned by another package is ignored", + pkgs: []*apk.InstalledPackage{ + installed("busybox", "1.36.1-r0", "bin/busybox"), + installed("planter", "1.0.0-r0", sbomAt("planter-1.0.0-r0"), sbomAt("busybox-1.36.1-r0")), + }, + files: map[string][]byte{ + sbomAt("planter-1.0.0-r0"): internalSBOM(t, record("planter", "1.0.0-r0")), + sbomAt("busybox-1.36.1-r0"): internalSBOM(t, record("busybox", "9.9.9-r0")), + }, + present: []string{"[email protected]", "[email protected]"}, + absent: []string{"[email protected]"}, + }, { + name: "SBOM describing another package fails", + pkgs: []*apk.InstalledPackage{ + installed("evil-helper", "1.0.0-r0", sbomAt("evil-helper-1.0.0-r0")), + }, + files: map[string][]byte{ + sbomAt("evil-helper-1.0.0-r0"): internalSBOM(t, + record("openssl", "9.9.9-r0", apkRef("openssl", "9.9.9-r0"))), + }, + wantErr: "evil-helper", + }, { + name: "SBOM describing another version fails", + pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))}, + files: map[string][]byte{sbomAt("foo-1.0.0-r0"): internalSBOM(t, record("foo", "2.0.0-r0"))}, + wantErr: "foo", + }, { + name: "described apk PURL naming another package fails", + pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))}, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): internalSBOM(t, + record("foo", "1.0.0-r0", apkRef("openssl", "3.0.1-r0"))), + }, + wantErr: "foo", + }, { + name: "reachable package with another package's apk PURL fails", + pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))}, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, + record("openssl", "9.9.9-r0", apkRef("openssl", "9.9.9-r0"))), + }, + wantErr: "foo", + }, { + name: "reachable package named for the package with another apk PURL fails", + pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))}, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, Package{ + ID: "SPDXRef-Package-apk-foo-0123", Name: "foo", Version: "1.0.0-r0", + ExternalRefs: []ExternalRef{apkRef("openssl", "3.0.1-r0")}, + }), + }, + wantErr: "foo", + }, { + name: "reachable package using an installed package's SPDX ID fails", + pkgs: []*apk.InstalledPackage{ + installed("aaa", "1.0.0-r0", sbomAt("aaa-1.0.0-r0")), + installed("openssl", "3.0.1-r0", sbomAt("openssl-3.0.1-r0")), + }, + files: map[string][]byte{ + sbomAt("aaa-1.0.0-r0"): internalSBOM(t, record("aaa", "1.0.0-r0"), + Package{ID: openssl.ID, Name: "openssl-src", Version: "3.5.0"}), + sbomAt("openssl-3.0.1-r0"): internalSBOM(t, openssl), + }, + wantErr: "aaa", + }, { + name: "described package using another installed package's SPDX ID fails", + pkgs: []*apk.InstalledPackage{ + installed("aaa", "1.0.0-r0", sbomAt("aaa-1.0.0-r0")), + installed("openssl", "3.0.1-r0", sbomAt("openssl-3.0.1-r0")), + }, + files: map[string][]byte{ + sbomAt("aaa-1.0.0-r0"): internalSBOM(t, + Package{ID: openssl.ID, Name: "aaa", Version: "1.0.0-r0"}), + sbomAt("openssl-3.0.1-r0"): internalSBOM(t, openssl), + }, + wantErr: "aaa", + }, { + name: "shared SPDX ID with a different license is kept once", + pkgs: []*apk.InstalledPackage{ + installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")), + installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")), + }, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source), + sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", "1.0.0-r0"), relicensed), + }, + present: []string{"[email protected]", "[email protected]"}, + }, { + name: "shared SPDX ID with reordered references and checksums is kept once", + pkgs: []*apk.InstalledPackage{ + installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")), + installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")), + }, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source), + sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", "1.0.0-r0"), reordered), + }, + present: []string{"[email protected]", "[email protected]"}, + }, { + name: "shared SPDX ID with a different version fails", + pkgs: []*apk.InstalledPackage{ + installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")), + installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")), + }, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source), + sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", "1.0.0-r0"), + Package{ID: source.ID, Name: "foo-src", Version: "v9.9.9"}), + }, + wantErr: source.ID, + }, { + name: "shared SPDX ID with a different PURL fails", + pkgs: []*apk.InstalledPackage{ + installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")), + installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")), + }, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source), + sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", "1.0.0-r0"), repurled), + }, + wantErr: source.ID, + }, { + name: "shared SPDX ID with a different checksum fails", + pkgs: []*apk.InstalledPackage{ + installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")), + installed("bar", "1.0.0-r0", sbomAt("bar-1.0.0-r0")), + }, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): internalSBOM(t, foo, source), + sbomAt("bar-1.0.0-r0"): internalSBOM(t, record("bar", "1.0.0-r0"), rehashed), + }, + wantErr: source.ID, + }, { + name: "malformed SBOM fails", + pkgs: []*apk.InstalledPackage{installed("broken", "1.0.0-r0", sbomAt("broken-1.0.0-r0"))}, + files: map[string][]byte{sbomAt("broken-1.0.0-r0"): []byte("{not json")}, + wantErr: "broken", + }, { + name: "SBOM that describes nothing fails", + pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))}, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): []byte(`{"SPDXID":"SPDXRef-DOCUMENT","packages":[` + + `{"SPDXID":"SPDXRef-Package-foo-1.0.0-r0","name":"foo","versionInfo":"1.0.0-r0"}]}`), + }, + wantErr: "foo", + }, { + name: "SBOM describing a missing element fails", + pkgs: []*apk.InstalledPackage{installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0"))}, + files: map[string][]byte{ + sbomAt("foo-1.0.0-r0"): []byte(`{"SPDXID":"SPDXRef-DOCUMENT",` + + `"documentDescribes":["SPDXRef-Package-ghost"],"packages":[]}`), + }, + wantErr: "foo", + }} { + t.Run(tt.name, func(t *testing.T) { + fsys := apkfs.NewMemFS() + require.NoError(t, fsys.MkdirAll("var/lib/db/sbom", 0o755)) + for p, b := range tt.files { + require.NoError(t, fsys.WriteFile(p, b, 0o644)) + } + opts := testOpts(fsys) + opts.Packages = tt.pkgs + + out := filepath.Join(t.TempDir(), "sbom.spdx.json") + err := New().Generate(t.Context(), opts, out) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + b, err := os.ReadFile(out) + require.NoError(t, err) + var doc Document + require.NoError(t, json.Unmarshal(b, &doc)) + entries := map[string]struct{}{} + purls := map[string]struct{}{} + for _, p := range doc.Packages { + entries[p.Name+"@"+p.Version] = struct{}{} + for _, ref := range p.ExternalRefs { + purls[ref.Locator] = struct{}{} + } + } + for _, want := range tt.present { + require.Contains(t, entries, want) + } + for _, notWant := range tt.absent { + require.NotContains(t, entries, notWant) + } + for _, want := range tt.purls { + require.Contains(t, purls, want) + } + }) + } +} + +// statErrFS fails every Stat, as a filesystem that denies access would. +type statErrFS struct{ apkfs.FullFS } + +func (statErrFS) Stat(string) (fs.FileInfo, error) { return nil, fs.ErrPermission } + +func TestLocateApkSBOMStatError(t *testing.T) { + ipkg := installed("foo", "1.0.0-r0", sbomAt("foo-1.0.0-r0")) + _, err := locateApkSBOM(statErrFS{apkfs.NewMemFS()}, ipkg) + require.ErrorIs(t, err, fs.ErrPermission) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/license.go new/apko-1.4.6/pkg/sbom/generator/spdx/license.go --- old/apko-1.4.5/pkg/sbom/generator/spdx/license.go 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.4.6/pkg/sbom/generator/spdx/license.go 2026-09-30 14:19:13.000000000 +0200 @@ -0,0 +1,123 @@ +// Copyright 2026 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spdx + +import ( + "crypto/sha256" + "encoding/hex" + "slices" + "strings" + + "github.com/github/go-spdx/v2/spdxexp" +) + +// licenseRefPrefix marks references built from apk database license fields, +// so they cannot collide with a reference an embedded SBOM defines. +const licenseRefPrefix = "LicenseRef-apk-" + +// licenseExpression converts an apk license field into a valid SPDX license +// expression. Most fields already are one. Others list licenses separated by +// spaces, use lowercase operators, or name licenses SPDX does not list; those +// names become references whose extracted text is the original name. +func licenseExpression(raw string) (string, []LicensingInfo) { + raw = strings.TrimSpace(raw) + switch { + case raw == "": + return NOASSERTION, nil + case isLicenseExpression(raw): + return raw, nil + } + + tokens := strings.Fields(strings.NewReplacer("(", " ( ", ")", " ) ").Replace(raw)) + bare := true + for i, tok := range tokens { + if op := strings.ToUpper(tok); op == "AND" || op == "OR" || op == "WITH" { + tokens[i], bare = op, false + } + } + if bare { + // A list naming every license that applies. One that names no listed + // license is a single name, such as "Public Domain". + if !slices.ContainsFunc(tokens, isLicenseExpression) { + return licenseRef(raw) + } + tokens = joinAnd(tokens) + } + + var refs []LicensingInfo + for i, tok := range tokens { + if isExpressionSyntax(tok) || (i > 0 && tokens[i-1] == "WITH") || isLicenseExpression(tok) { + continue + } + id := licenseRefID(tok) + tokens[i] = id + if !slices.ContainsFunc(refs, func(r LicensingInfo) bool { return r.LicenseID == id }) { + refs = append(refs, LicensingInfo{LicenseID: id, ExtractedText: tok}) + } + } + + expr := strings.NewReplacer("( ", "(", " )", ")").Replace(strings.Join(tokens, " ")) + if !isLicenseExpression(expr) { + return licenseRef(raw) + } + return expr, refs +} + +func isLicenseExpression(s string) bool { + ok, _ := spdxexp.ValidateLicenses([]string{s}) + return ok +} + +func isExpressionSyntax(tok string) bool { + switch tok { + case "AND", "OR", "WITH", "(", ")": + return true + } + return false +} + +func joinAnd(licenses []string) []string { + out := make([]string, 0, 2*len(licenses)-1) + for i, l := range licenses { + if i > 0 { + out = append(out, "AND") + } + out = append(out, l) + } + return out +} + +// licenseRef describes all of raw as one unlisted license. +func licenseRef(raw string) (string, []LicensingInfo) { + id := licenseRefID(raw) + return id, []LicensingInfo{{LicenseID: id, ExtractedText: raw}} +} + +// licenseRefID derives a reference ID from name, keeping the characters SPDX +// allows in an ID and hashing a name that has none. +func licenseRefID(name string) string { + id := strings.Trim(strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '-': + return r + } + return '-' + }, name), "-") + if id == "" { + h := sha256.Sum256([]byte(name)) + id = hex.EncodeToString(h[:8]) + } + return licenseRefPrefix + id +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/license_test.go new/apko-1.4.6/pkg/sbom/generator/spdx/license_test.go --- old/apko-1.4.5/pkg/sbom/generator/spdx/license_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.4.6/pkg/sbom/generator/spdx/license_test.go 2026-09-30 14:19:13.000000000 +0200 @@ -0,0 +1,153 @@ +// Copyright 2026 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spdx + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/github/go-spdx/v2/spdxexp" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + + "chainguard.dev/apko/pkg/apk/apk" + apkfs "chainguard.dev/apko/pkg/apk/fs" +) + +func TestLicenseExpression(t *testing.T) { + ref := func(id, text string) []LicensingInfo { + return []LicensingInfo{{LicenseID: id, ExtractedText: text}} + } + for _, tt := range []struct { + name string + raw string + want string + refs []LicensingInfo + }{{ + name: "empty field is no assertion", + raw: "", + want: NOASSERTION, + }, { + name: "listed license is kept", + raw: "MIT", + want: "MIT", + }, { + name: "valid expression is kept", + raw: "MIT AND BSD-2-Clause", + want: "MIT AND BSD-2-Clause", + }, { + name: "listed exception is kept", + raw: "GPL-2.0-or-later WITH Autoconf-exception-2.0", + want: "GPL-2.0-or-later WITH Autoconf-exception-2.0", + }, { + name: "nested expression is kept", + raw: "(MIT OR Apache-2.0) AND Zlib", + want: "(MIT OR Apache-2.0) AND Zlib", + }, { + name: "lowercase operator is uppercased", + raw: "BSD-2-Clause AND CC-BY-SA-4.0 and CC0-1.0", + want: "BSD-2-Clause AND CC-BY-SA-4.0 AND CC0-1.0", + }, { + name: "space-separated list is joined with AND", + raw: "Apache-2.0 MIT", + want: "Apache-2.0 AND MIT", + }, { + name: "unlisted license in a list becomes a reference", + raw: "BSD MIT", + want: "LicenseRef-apk-BSD AND MIT", + refs: ref("LicenseRef-apk-BSD", "BSD"), + }, { + name: "unlisted license in an expression becomes a reference", + raw: "(BSD-2-Clause OR custom) AND MIT", + want: "(BSD-2-Clause OR LicenseRef-apk-custom) AND MIT", + refs: ref("LicenseRef-apk-custom", "custom"), + }, { + name: "repeated unlisted license is referenced once", + raw: "custom AND GPL-2.0-only AND custom", + want: "LicenseRef-apk-custom AND GPL-2.0-only AND LicenseRef-apk-custom", + refs: ref("LicenseRef-apk-custom", "custom"), + }, { + name: "unlisted license alone becomes a reference", + raw: "custom:chromiumos", + want: "LicenseRef-apk-custom-chromiumos", + refs: ref("LicenseRef-apk-custom-chromiumos", "custom:chromiumos"), + }, { + name: "list naming no listed license is one name", + raw: "Public Domain", + want: "LicenseRef-apk-Public-Domain", + refs: ref("LicenseRef-apk-Public-Domain", "Public Domain"), + }, { + name: "malformed expression becomes one reference", + raw: "MIT AND", + want: "LicenseRef-apk-MIT-AND", + refs: ref("LicenseRef-apk-MIT-AND", "MIT AND"), + }, { + name: "unlisted exception becomes one reference", + raw: "GPL-2.0-or-later WITH custom-exception", + want: "LicenseRef-apk-GPL-2.0-or-later-WITH-custom-exception", + refs: ref("LicenseRef-apk-GPL-2.0-or-later-WITH-custom-exception", "GPL-2.0-or-later WITH custom-exception"), + }} { + t.Run(tt.name, func(t *testing.T) { + got, refs := licenseExpression(tt.raw) + if got != tt.want { + t.Errorf("expression: got = %q, want = %q", got, tt.want) + } + if diff := cmp.Diff(tt.refs, refs); diff != "" { + t.Errorf("references (-want, +got):\n%s", diff) + } + if got != NOASSERTION { + if ok, bad := spdxexp.ValidateLicenses([]string{got}); !ok { + t.Errorf("%q is not a valid SPDX license expression: %q", got, bad) + } + } + }) + } +} + +func TestLicenseExpressionUnnamed(t *testing.T) { + got, refs := licenseExpression("???") + require.True(t, strings.HasPrefix(got, "LicenseRef-apk-"), got) + require.Equal(t, []LicensingInfo{{LicenseID: got, ExtractedText: "???"}}, refs) + ok, bad := spdxexp.ValidateLicenses([]string{got}) + require.True(t, ok, bad) +} + +func TestInstalledPackageLicense(t *testing.T) { + fsys := apkfs.NewMemFS() + opts := testOpts(fsys) + gpl := installed("gpl-tool", "1.0.0-r0") + gpl.License = "GPL-3.0-or-later custom" + vendor := installed("vendor-tool", "2.0.0-r0") + vendor.License = "custom" + opts.Packages = []*apk.InstalledPackage{gpl, vendor} + + out := filepath.Join(t.TempDir(), "sbom.spdx.json") + require.NoError(t, New().Generate(t.Context(), opts, out)) + b, err := os.ReadFile(out) + require.NoError(t, err) + var doc Document + require.NoError(t, json.Unmarshal(b, &doc)) + + declared := map[string]string{} + for _, p := range doc.Packages { + declared[p.Name] = p.LicenseDeclared + } + require.Equal(t, "GPL-3.0-or-later AND LicenseRef-apk-custom", declared["gpl-tool"]) + require.Equal(t, "LicenseRef-apk-custom", declared["vendor-tool"]) + require.Equal(t, []LicensingInfo{{LicenseID: "LicenseRef-apk-custom", ExtractedText: "custom"}}, doc.LicensingInfos) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/spdx.go new/apko-1.4.6/pkg/sbom/generator/spdx/spdx.go --- old/apko-1.4.5/pkg/sbom/generator/spdx/spdx.go 2026-09-22 00:49:38.000000000 +0200 +++ new/apko-1.4.6/pkg/sbom/generator/spdx/spdx.go 2026-09-30 14:19:13.000000000 +0200 @@ -15,13 +15,17 @@ package spdx import ( + "cmp" "context" "encoding/json" "errors" "fmt" + "io/fs" "os" + "path" + "reflect" "regexp" - "sort" + "slices" "strings" "time" "unicode/utf8" @@ -29,6 +33,7 @@ "github.com/chainguard-dev/clog" v1 "github.com/google/go-containerregistry/pkg/v1" purl "github.com/package-url/packageurl-go" + "k8s.io/apimachinery/pkg/util/sets" "sigs.k8s.io/release-utils/version" "chainguard.dev/apko/pkg/apk/apk" @@ -154,21 +159,38 @@ } } + reserved := reservedIDs(opts.Packages) for _, pkg := range opts.Packages { - // Check to see if the apk contains an sbom describing itself - if err := sx.ProcessInternalApkSBOM(ctx, opts, doc, pkg); err != nil { - return fmt.Errorf("parsing internal apk SBOM: %w", err) + if err := sx.processInternalApkSBOM(ctx, opts, doc, pkg, reserved); err != nil { + return fmt.Errorf("describing package %q: %w", pkg.Name+"-"+pkg.Version, err) } } + // Packages built from the same origin or upstream source share records, so + // keep one copy. Two records under one ID that identify different components + // would let one package's SBOM displace another's, so refuse them. Builds of + // one source can still disagree on metadata such as its license; keep the + // first record then. dedupedPackages := make([]Package, 0, len(doc.Packages)) - seenIDs := make(map[string]struct{}) - for i := range doc.Packages { - if _, ok := seenIDs[doc.Packages[i].ID]; !ok { - seenIDs[doc.Packages[i].ID] = struct{}{} - dedupedPackages = append(dedupedPackages, doc.Packages[i]) - } else { - clog.FromContext(ctx).Debug("duplicate package ID found in SBOM, deduplicating package...", "ID", doc.Packages[i].ID) + seen := make(map[string]int, len(doc.Packages)) + for _, p := range doc.Packages { + j, ok := seen[p.ID] + if !ok { + seen[p.ID] = len(dedupedPackages) + dedupedPackages = append(dedupedPackages, p) + continue + } + prev := dedupedPackages[j] + switch { + case !sameIdentity(prev, p): + return fmt.Errorf("SPDX ID %q names two packages that differ in name, version, "+ + "external references, or checksums: %q and %q", + p.ID, prev.Name+"@"+prev.Version, p.Name+"@"+p.Version) + case !sameMetadata(prev, p): + clog.WarnContext(ctx, "records sharing an SPDX ID disagree on metadata; keeping the first", + "ID", p.ID, "package", p.Name+"@"+p.Version) + default: + clog.DebugContext(ctx, "duplicate package ID found in SBOM, deduplicating package...", "ID", p.ID) } } doc.Packages = dedupedPackages @@ -180,25 +202,53 @@ return nil } -// locateApkSBOM returns the path to the SBOM in the given filesystem, using the -// given Package's name and version. It returns an empty string if the SBOM is -// not found. +// sameIdentity reports whether a and b identify one component to a scanner: +// the same name, version, external references, and digests, in any order. +func sameIdentity(a, b Package) bool { + return a.Name == b.Name && a.Version == b.Version && + sets.New(a.ExternalRefs...).Equal(sets.New(b.ExternalRefs...)) && + sets.New(a.Checksums...).Equal(sets.New(b.Checksums...)) && + reflect.DeepEqual(a.VerificationCode, b.VerificationCode) +} + +// sameMetadata reports whether a and b, which share an identity, also agree on +// every other field. +func sameMetadata(a, b Package) bool { + a.ExternalRefs, a.Checksums = b.ExternalRefs, b.Checksums + return reflect.DeepEqual(a, b) +} + +// epochRe matches the -rN epoch that ends an apk version. +var epochRe = regexp.MustCompile(`-r\d+$`) + +// locateApkSBOM returns the path of the SBOM that ipkg ships, or "" if it ships +// none. Only paths the installed database lists for ipkg count, so an SBOM that +// another package installs under ipkg's name is ignored. func locateApkSBOM(fsys apkfs.ReaderFS, ipkg *apk.InstalledPackage) (string, error) { - re := regexp.MustCompile(`-r\d+$`) + owned := map[string]struct{}{} + for _, f := range ipkg.Files { + if p := path.Clean("/" + f.Name); path.Dir(p) == apkSBOMdir { + owned[p] = struct{}{} + } + } + for _, s := range []string{ fmt.Sprintf("%s/%s-%s.spdx.json", apkSBOMdir, ipkg.Name, ipkg.Version), - fmt.Sprintf("%s/%s-%s.spdx.json", apkSBOMdir, ipkg.Name, re.ReplaceAllString(ipkg.Version, "")), + fmt.Sprintf("%s/%s-%s.spdx.json", apkSBOMdir, ipkg.Name, epochRe.ReplaceAllString(ipkg.Version, "")), fmt.Sprintf("%s/%s.spdx.json", apkSBOMdir, ipkg.Name), } { + if _, ok := owned[s]; !ok { + continue + } info, err := fsys.Stat(s) + if errors.Is(err, fs.ErrNotExist) { + continue + } if err != nil { - if os.IsNotExist(err) { - continue - } + return "", fmt.Errorf("inspecting %q: %w", s, err) } - if info.IsDir() { - return "", fmt.Errorf("directory found at SBOM path %s", s) + return "", fmt.Errorf("directory found at SBOM path %q", s) } return s, nil } @@ -206,104 +256,233 @@ return "", nil } +// ProcessInternalApkSBOM adds to doc the packages that ipkg's own SBOM +// describes and everything they reach, or a record built from the apk database +// when ipkg ships no SBOM. It fails when the SBOM does not parse or describes +// anything other than ipkg. func (sx *SPDX) ProcessInternalApkSBOM(ctx context.Context, opts *options.Options, doc *Document, ipkg *apk.InstalledPackage) error { - // Check if apk installed an SBOM - path, err := locateApkSBOM(opts.FS, ipkg) + return sx.processInternalApkSBOM(ctx, opts, doc, ipkg, reservedIDs(opts.Packages)) +} + +func (sx *SPDX) processInternalApkSBOM(ctx context.Context, opts *options.Options, doc *Document, ipkg *apk.InstalledPackage, reserved map[string]struct{}) error { + sbomPath, err := locateApkSBOM(opts.FS, ipkg) if err != nil { return fmt.Errorf("inspecting FS for internal apk SBOM: %w", err) } - if path == "" { - // The SBOM does not exist. - // (So just ignore that the package was specified to the SPDX Generate method?) + if sbomPath == "" { + clog.WarnContext(ctx, "package ships no SBOM; describing it from the apk database", + "package", ipkg.Name, "version", ipkg.Version) + p, licenses := installedPackage(opts, ipkg) + doc.Packages = append(doc.Packages, p) + mergeLicensingInfos(ctx, &Document{LicensingInfos: licenses}, doc) + addContains(doc, []string{p.ID}) return nil } - apkSBOMDoc, err := sx.ParseInternalSBOM(opts, path) + apkSBOMDoc, err := sx.ParseInternalSBOM(opts, sbomPath) if err != nil { - // TODO: Log error parsing apk SBOM - return nil + return err } - // Cycle the top level elements... - // Find elements described by the document - check both documentDescribes array - // and DESCRIBES relationships (from SPDXRef-DOCUMENT) - idsDescribedByAPKSBOM := map[string]struct{}{} - - // First check documentDescribes array - for _, elementID := range apkSBOMDoc.DocumentDescribes { - idsDescribedByAPKSBOM[elementID] = struct{}{} + described := describedIDs(apkSBOMDoc) + if len(described) == 0 { + return fmt.Errorf("%q describes no package", sbomPath) + } + todo := reachableIDs(apkSBOMDoc, described) + if err := checkIdentity(apkSBOMDoc, ipkg, described, todo, reserved); err != nil { + return fmt.Errorf("checking %q: %w", sbomPath, err) + } + if err := copySBOMElements(apkSBOMDoc, doc, todo); err != nil { + return fmt.Errorf("copying element: %w", err) } - // Also check for DESCRIBES relationships from SPDXRef-DOCUMENT - for _, rel := range apkSBOMDoc.Relationships { - if rel.Element == "SPDXRef-DOCUMENT" && rel.Type == "DESCRIBES" { - idsDescribedByAPKSBOM[rel.Related] = struct{}{} - } + mergeLicensingInfos(ctx, apkSBOMDoc, doc) + addContains(doc, described) + + return nil +} + +// packageID is the SPDX ID melange gives an apk's own record. +func packageID(ipkg *apk.InstalledPackage) string { + return stringToIdentifier(fmt.Sprintf("SPDXRef-Package-%s-%s", ipkg.Name, ipkg.Version)) +} + +// reservedIDs returns the SPDX ID of each installed package's own record. +func reservedIDs(pkgs []*apk.InstalledPackage) map[string]struct{} { + ids := make(map[string]struct{}, len(pkgs)) + for _, p := range pkgs { + ids[packageID(p)] = struct{}{} } + return ids +} - // ... searching for a 1st level package - targetElementIDs := map[string]struct{}{} - for _, pkg := range apkSBOMDoc.Packages { - if _, ok := idsDescribedByAPKSBOM[pkg.ID]; !ok { - continue +// describedIDs returns, sorted, the elements a document names through +// documentDescribes or a DESCRIBES relationship from the document itself. +func describedIDs(d *Document) []string { + ids := slices.Clone(d.DocumentDescribes) + for _, r := range d.Relationships { + if r.Element == "SPDXRef-DOCUMENT" && r.Type == "DESCRIBES" { + ids = append(ids, r.Related) } + } + slices.Sort(ids) + return slices.Compact(ids) +} - targetElementIDs[pkg.ID] = struct{}{} - if len(targetElementIDs) == len(idsDescribedByAPKSBOM) { - // Exit early if we found them all. - break +// reachableIDs returns roots and every element they reach through +// relationships, other than files. +func reachableIDs(d *Document, roots []string) map[string]struct{} { + edges := make(map[string][]string, len(d.Relationships)) + for _, r := range d.Relationships { + if !strings.HasPrefix(r.Related, "SPDXRef-File-") { + edges[r.Element] = append(edges[r.Element], r.Related) } } - sortedTargetElementIDs := make([]string, 0, len(targetElementIDs)) - for id := range targetElementIDs { - sortedTargetElementIDs = append(sortedTargetElementIDs, id) + seen := make(map[string]struct{}, len(roots)) + queue := slices.Clone(roots) + for len(queue) > 0 { + id := queue[0] + queue = queue[1:] + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + queue = append(queue, edges[id]...) } - // Sort the element IDs so repeated builds produce the same relationship order. - sort.Strings(sortedTargetElementIDs) + return seen +} - todo := make(map[string]struct{}, len(apkSBOMDoc.Relationships)) - for _, id := range sortedTargetElementIDs { - todo[id] = struct{}{} +// checkIdentity confirms that d describes ipkg alone: every described package +// and every package with an apk PURL names ipkg at its version, and nothing it +// reaches claims the SPDX ID of an installed package. +func checkIdentity(d *Document, ipkg *apk.InstalledPackage, described []string, reach, reserved map[string]struct{}) error { + own := packageID(ipkg) + byID := make(map[string]*Package, len(d.Packages)) + for i := range d.Packages { + byID[d.Packages[i].ID] = &d.Packages[i] } - if err := copySBOMElements(apkSBOMDoc, doc, todo); err != nil { - return fmt.Errorf("copying element: %w", err) + for _, id := range described { + p, ok := byID[id] + if !ok { + return fmt.Errorf("described element %q is not a package", id) + } + if !namesPackage(p.Name, p.Version, ipkg) { + return fmt.Errorf("describes %q at %q rather than %q at %q", p.Name, p.Version, ipkg.Name, ipkg.Version) + } + purls, err := apkPURLs(p) + if err != nil { + return err + } + for _, u := range purls { + if !namesPackage(u.Name, u.Version, ipkg) { + return fmt.Errorf("package %q carries PURL %q", id, u.String()) + } + } + if _, ok := reserved[id]; ok && id != own { + return fmt.Errorf("package %q uses the SPDX ID of another installed package", id) + } } - mergeLicensingInfos(ctx, apkSBOMDoc, doc) - - // Add CONTAINS relationships from the document root package to all top-level elements from the internal SBOM. - // This ensures they are reachable from the document root for tools that traverse the SBOM graph. - if len(doc.DocumentDescribes) > 0 { - rootPkgID := doc.DocumentDescribes[0] - for _, elementID := range sortedTargetElementIDs { - doc.Relationships = append(doc.Relationships, Relationship{ - Element: rootPkgID, - Type: "CONTAINS", - Related: elementID, - }) + for i := range d.Packages { + p := &d.Packages[i] + if _, ok := reach[p.ID]; !ok || slices.Contains(described, p.ID) { + continue + } + if _, ok := reserved[p.ID]; ok || p.ID == own { + return fmt.Errorf("reachable package %q uses the SPDX ID of an installed package", p.ID) + } + purls, err := apkPURLs(p) + if err != nil { + return err + } + // Some generators catalog the package's own apk entry as a reachable package. + for _, u := range purls { + if !namesPackage(p.Name, p.Version, ipkg) || !namesPackage(u.Name, u.Version, ipkg) { + return fmt.Errorf("reachable package %q carries apk PURL %q", p.ID, u.String()) + } } } return nil } -func copySBOMElements(sourceDoc, targetDoc *Document, todo map[string]struct{}) error { - // Walk the graph looking for things to copy. - // Loop until we don't find any new todos. - for prev, next := 0, len(todo); next != prev; prev, next = next, len(todo) { - for _, r := range sourceDoc.Relationships { - if strings.HasPrefix(r.Related, "SPDXRef-File-") { - continue - } - if _, ok := todo[r.Element]; ok { - todo[r.Related] = struct{}{} +// namesPackage reports whether name and version identify ipkg, with or without +// its epoch. +func namesPackage(name, version string, ipkg *apk.InstalledPackage) bool { + return name == ipkg.Name && + (version == ipkg.Version || version == epochRe.ReplaceAllString(ipkg.Version, "")) +} + +// apkPURLs returns the pkg:apk PURLs among p's external references. +func apkPURLs(p *Package) ([]purl.PackageURL, error) { + var out []purl.PackageURL + for _, ref := range p.ExternalRefs { + if ref.Type != ExtRefTypePurl { + continue + } + u, err := purl.FromString(ref.Locator) + if err != nil { + if strings.HasPrefix(ref.Locator, "pkg:apk/") { + return nil, fmt.Errorf("package %q carries malformed PURL %q: %w", p.ID, ref.Locator, err) } + continue + } + if u.Type == "apk" { + out = append(out, u) } } + return out, nil +} - // Now copy everything over. +// installedPackage describes ipkg from its apk database entry, returning the +// extracted licenses its license expression references. +func installedPackage(opts *options.Options, ipkg *apk.InstalledPackage) (Package, []LicensingInfo) { + qualifiers := map[string]string{} + if arch := cmp.Or(ipkg.Arch, opts.ImageInfo.Arch.ToAPK()); arch != "" { + qualifiers["arch"] = arch + } + license, refs := licenseExpression(ipkg.License) + return Package{ + ID: packageID(ipkg), + Name: ipkg.Name, + Version: ipkg.Version, + LicenseConcluded: NOASSERTION, + LicenseDeclared: license, + Description: ipkg.Description, + DownloadLocation: NOASSERTION, + Originator: supplier(opts), + Supplier: supplier(opts), + SourceInfo: "Package info from apk database", + CopyrightText: NOASSERTION, + ExternalRefs: []ExternalRef{{ + Category: ExtRefPackageManager, + Type: ExtRefTypePurl, + Locator: purl.NewPackageURL("apk", opts.OS.ID, ipkg.Name, ipkg.Version, + purl.QualifiersFromMap(qualifiers), "").String(), + }}, + }, refs +} + +// addContains links the document root to each of ids, so tools that walk the +// graph from the root reach them. +func addContains(doc *Document, ids []string) { + if len(doc.DocumentDescribes) == 0 { + return + } + for _, id := range ids { + doc.Relationships = append(doc.Relationships, Relationship{ + Element: doc.DocumentDescribes[0], + Type: "CONTAINS", + Related: id, + }) + } +} + +// copySBOMElements copies the packages in todo, and the relationships from +// them other than to files, from sourceDoc to targetDoc. +func copySBOMElements(sourceDoc, targetDoc *Document, todo map[string]struct{}) error { done := make(map[string]struct{}, len(todo)) for _, p := range sourceDoc.Packages { @@ -365,7 +544,7 @@ } if err := json.Unmarshal(data, internalSBOM); err != nil { - return nil, fmt.Errorf("parsing internal apk sbom: %w", err) + return nil, fmt.Errorf("parsing internal apk sbom %q: %w", path, err) } // Fix up missing data, checkers require Originator & diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/spdx_test.go new/apko-1.4.6/pkg/sbom/generator/spdx/spdx_test.go --- old/apko-1.4.5/pkg/sbom/generator/spdx/spdx_test.go 2026-09-22 00:49:38.000000000 +0200 +++ new/apko-1.4.6/pkg/sbom/generator/spdx/spdx_test.go 2026-09-30 14:19:13.000000000 +0200 @@ -15,6 +15,7 @@ package spdx import ( + "archive/tar" "encoding/json" "fmt" "os" @@ -218,7 +219,7 @@ fsys := apkfs.NewMemFS() tt.opts.FS = fsys sbomDir := path.Join("var", "lib", "db", "sbom") - err := fsys.MkdirAll(sbomDir, 0750) + err := fsys.MkdirAll(sbomDir, 0o750) require.NoError(t, err) for _, apkPkg := range tt.opts.Packages { @@ -228,8 +229,9 @@ require.NoError(t, err) sbomDestPath := path.Join(sbomDir, apkSBOMName) - err = fsys.WriteFile(sbomDestPath, apkSBOMBytes, 0644) + err = fsys.WriteFile(sbomDestPath, apkSBOMBytes, 0o644) require.NoError(t, err) + apkPkg.Files = append(apkPkg.Files, tar.Header{Name: sbomDestPath}) } sx := New() @@ -281,7 +283,11 @@ dir := t.TempDir() fsys := apkfs.NewMemFS() opts := testOpts(fsys) - opts.Packages = []*apk.InstalledPackage{{Name: "glibc", Version: "2.40-r0"}} + opts.Packages = []*apk.InstalledPackage{{ + Name: "glibc", + Version: "2.40-r0", + Files: []tar.Header{{Name: internalSBOMPath}}, + }} internalSBOM := Document{ DocumentDescribes: []string{ @@ -289,14 +295,15 @@ documentRootID, }, Packages: []Package{ - {ID: packageID, Name: "glibc"}, - {ID: documentRootID, Name: "/"}, + // Every described element must name the installed package. + {ID: packageID, Name: "glibc", Version: "2.40-r0"}, + {ID: documentRootID, Name: "glibc", Version: "2.40-r0"}, }, } data, err := json.Marshal(internalSBOM) require.NoError(t, err) - require.NoError(t, fsys.MkdirAll("/var/lib/db/sbom", 0750)) - require.NoError(t, fsys.WriteFile(internalSBOMPath, data, 0644)) + require.NoError(t, fsys.MkdirAll("/var/lib/db/sbom", 0o750)) + require.NoError(t, fsys.WriteFile(internalSBOMPath, data, 0o644)) sx := New() generate := func(i int) []byte { @@ -353,7 +360,7 @@ } func TestStringToIdentifier(t *testing.T) { - var validIDRe = regexp.MustCompile(`^[a-zA-Z0-9-.]+$`) + validIDRe := regexp.MustCompile(`^[a-zA-Z0-9-.]+$`) for _, tc := range []string{ "alpine", "kindest/node:v1.21.1", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.4.5/pkg/sbom/generator/spdx/testdata/apk_sboms/wolfi-baselayout-20230201-r30.spdx.json new/apko-1.4.6/pkg/sbom/generator/spdx/testdata/apk_sboms/wolfi-baselayout-20230201-r30.spdx.json --- old/apko-1.4.5/pkg/sbom/generator/spdx/testdata/apk_sboms/wolfi-baselayout-20230201-r30.spdx.json 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.4.6/pkg/sbom/generator/spdx/testdata/apk_sboms/wolfi-baselayout-20230201-r30.spdx.json 2026-09-30 14:19:13.000000000 +0200 @@ -0,0 +1,122 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "apk-wolfi-baselayout-20230201-r30", + "documentNamespace": "https://spdx.org/spdxdocs/chainguard/build/b5df68054c4f3e4ad79714cd9a3d39ca", + "creationInfo": { + "licenseListVersion": "3.22", + "creators": [ + "Tool: chainguard-build (v0.59.4)", + "Organization: Chainguard, Inc" + ], + "created": "2026-09-09T19:39:56Z" + }, + "packages": [ + { + "name": "wolfi", + "SPDXID": "SPDXRef-OperatingSystem", + "supplier": "Organization: Wolfi", + "originator": "Organization: Wolfi", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "description": "Operating System", + "primaryPackagePurpose": "OPERATING-SYSTEM" + }, + { + "name": "wolfi-baselayout", + "SPDXID": "SPDXRef-Package-apk-wolfi-baselayout-20230201-r30", + "versionInfo": "20230201-r30", + "supplier": "Organization: Wolfi", + "originator": "Organization: Wolfi", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/wolfi/wolfi-baselayout@20230201-r30?arch=x86_64\u0026distro=wolfi" + } + ], + "primaryPackagePurpose": "APPLICATION" + }, + { + "name": "wolfi-baselayout.yaml", + "SPDXID": "SPDXRef-Package-Melange-wolfi-baselayout.yaml-a4472f6292a5e48d6d611bf24c6e119b1a483e66", + "versionInfo": "a4472f6292a5e48d6d611bf24c6e119b1a483e66", + "supplier": "Organization: Wolfi", + "originator": "Organization: Wolfi", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "Apache-2.0", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:github/chainguard-dev/stereo@a4472f6292a5e48d6d611bf24c6e119b1a483e66#wolfi-baselayout.yaml" + } + ], + "primaryPackagePurpose": "INSTALL" + }, + { + "name": "wolfi-baselayout", + "SPDXID": "SPDXRef-Package-apk-wolfi-baselayout-d9db04555c7c746a", + "versionInfo": "20230201-r30", + "supplier": "Organization: chainguard", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "sourceInfo": "acquired package info from APK DB: /.PKGINFO", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION", + "description": "baselayout data for Wolfi", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/wolfi/wolfi-baselayout@20230201-r30?arch=x86_64\u0026origin=wolfi-baselayout" + } + ] + }, + { + "name": "wolfi-baselayout", + "SPDXID": "SPDXRef-DocumentRoot-Directory-wolfi-baselayout", + "versionInfo": "20230201-r30", + "supplier": "Organization: chainguard", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "primaryPackagePurpose": "FILE" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Package-apk-wolfi-baselayout-20230201-r30", + "relatedSpdxElement": "SPDXRef-Package-Melange-wolfi-baselayout.yaml-a4472f6292a5e48d6d611bf24c6e119b1a483e66", + "relationshipType": "DESCRIBED_BY" + }, + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Package-apk-wolfi-baselayout-20230201-r30", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-DocumentRoot-Directory-wolfi-baselayout", + "relatedSpdxElement": "SPDXRef-Package-apk-wolfi-baselayout-d9db04555c7c746a", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-apk-wolfi-baselayout-20230201-r30", + "relatedSpdxElement": "SPDXRef-DocumentRoot-Directory-wolfi-baselayout", + "relationshipType": "CONTAINS" + } + ] +} ++++++ apko.obsinfo ++++++ --- /var/tmp/diff_new_pack.Jc31Qi/_old 2026-10-01 16:47:31.547249644 +0200 +++ /var/tmp/diff_new_pack.Jc31Qi/_new 2026-10-01 16:47:31.554249938 +0200 @@ -1,5 +1,5 @@ name: apko -version: 1.4.5 -mtime: 1790030978 -commit: 739e7ce3f675ffb232e16849b51ff93657f1570c +version: 1.4.6 +mtime: 1790770753 +commit: 51c298aa5249f43193e3cb8684a2a60facfcb69d ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/apko/vendor.tar.gz /work/SRC/openSUSE:Factory/.apko.new.1253/vendor.tar.gz differ: char 34, line 2
