diff --git a/.github/workflows/publish-ubuntu-security-backports.yml b/.github/workflows/publish-ubuntu-security-backports.yml index a6d2826ac0..32e87fdf9a 100644 --- a/.github/workflows/publish-ubuntu-security-backports.yml +++ b/.github/workflows/publish-ubuntu-security-backports.yml @@ -30,6 +30,43 @@ jobs: python3 -B server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --check-lock + - name: Check native glibc OpenVEX semantics without package downloads + id: vex_semantics + env: + TRIVY_IMAGE: ghcr.io/aquasecurity/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 + run: | + set -Eeuo pipefail + helper=server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py + work="$(python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --print-work-root)" + fixture="$work/glibc-vex-semantic" + printf 'fixture=%s\n' "$fixture" >>"$GITHUB_OUTPUT" + python3 -B "$helper" --prepare-source + docker run --rm --platform linux/amd64 -v "$fixture:/work" "$TRIVY_IMAGE" --version >"$fixture/trivy-version.txt" + docker run --rm --platform linux/amd64 -v "$fixture:/work" "$TRIVY_IMAGE" \ + image --cache-dir /work/cache --download-db-only 2>&1 | tee "$fixture/database.log" + for mode in raw effective; do + vex=() + if [[ "$mode" == effective ]]; then vex=(--vex /work/scoped.openvex.json); fi + docker run --rm --platform linux/amd64 -v "$fixture:/work" "$TRIVY_IMAGE" \ + sbom --cache-dir /work/cache --skip-db-update --scanners vuln --format json \ + --output "/work/$mode.json" "${vex[@]}" /work/fixture.cdx.json \ + 2>&1 | tee "$fixture/$mode.log" + if [[ "$mode" == raw ]]; then python3 -B "$helper" --record-raw; fi + done + python3 -B "$helper" --verify + + - name: Retain native VEX matcher controls including failures + if: always() && steps.vex_semantics.outputs.fixture != '' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: glibc-nscd-native-vex-${{ github.sha }}-${{ github.run_id }} + path: | + ${{ steps.vex_semantics.outputs.fixture }}/*.json + ${{ steps.vex_semantics.outputs.fixture }}/*.txt + ${{ steps.vex_semantics.outputs.fixture }}/*.log + if-no-files-found: warn + retention-days: 30 + publish: needs: checks if: github.event_name == 'workflow_dispatch' && github.repository == 'PastureStack/server' && github.ref == 'refs/heads/main' @@ -116,6 +153,30 @@ jobs: python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --qualify \ 2>&1 | tee "$WORK/publisher/qualification.log" + - name: Require native exact-package glibc OpenVEX semantics + working-directory: source + env: + WORK: ${{ steps.inputs.outputs.work }} + TRIVY_IMAGE: ghcr.io/aquasecurity/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 + run: | + set -Eeuo pipefail + helper=server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py + fixture="$WORK/glibc-vex-semantic" + python3 -B "$helper" --prepare + docker run --rm --platform linux/amd64 -v "$fixture:/work" "$TRIVY_IMAGE" --version >"$fixture/trivy-version.txt" + docker run --rm --platform linux/amd64 -v "$fixture:/work" "$TRIVY_IMAGE" \ + image --cache-dir /work/cache --download-db-only 2>&1 | tee "$fixture/database.log" + for mode in raw effective; do + vex=() + if [[ "$mode" == effective ]]; then vex=(--vex /work/scoped.openvex.json); fi + docker run --rm --platform linux/amd64 -v "$fixture:/work" "$TRIVY_IMAGE" \ + sbom --cache-dir /work/cache --skip-db-update --scanners vuln --format json \ + --output "/work/$mode.json" "${vex[@]}" /work/fixture.cdx.json \ + 2>&1 | tee "$fixture/$mode.log" + if [[ "$mode" == raw ]]; then python3 -B "$helper" --record-raw; fi + done + python3 -B "$helper" --verify + - name: Require real installation in both isolated Ubuntu provider fixtures working-directory: source env: @@ -177,7 +238,19 @@ jobs: set -Eeuo pipefail mkdir "$WORK/anonymous-docker" "$WORK/readback" printf '{"auths":{}}\n' >"$WORK/anonymous-docker/config.json" - python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --verify-anonymous-manifest + public_ready=0 + for attempt in {1..20}; do + if python3 -B server/security/ubuntu-backports/bundle/qualify-native-packages.py --verify-anonymous-manifest; then + public_ready=1 + break + fi + printf 'Anonymous exact manifest not ready (attempt %s/20).\n' "$attempt" + if [[ "$attempt" -lt 20 ]]; then sleep 10; fi + done + if [[ "$public_ready" != 1 ]]; then + printf 'Anonymous exact manifest did not become available within the bounded readiness window.\n' >&2 + exit 1 + fi docker --config "$WORK/anonymous-docker" pull "$IMAGE@$IMAGE_DIGEST" container="$(docker create --entrypoint /unused "$IMAGE@$IMAGE_DIGEST")" trap 'docker rm "$container" >/dev/null' EXIT @@ -202,6 +275,9 @@ jobs: ${{ steps.inputs.outputs.work }}/publisher/ ${{ steps.inputs.outputs.work }}/consumer-gnu/ ${{ steps.inputs.outputs.work }}/consumer-uutils/ + ${{ steps.inputs.outputs.work }}/glibc-vex-semantic/*.json + ${{ steps.inputs.outputs.work }}/glibc-vex-semantic/*.txt + ${{ steps.inputs.outputs.work }}/glibc-vex-semantic/*.log ${{ steps.inputs.outputs.work }}/inputs/*-api.json if-no-files-found: warn retention-days: 30 diff --git a/server/security/ubuntu-backports/bundle/glibc-nscd-source-closure.json b/server/security/ubuntu-backports/bundle/glibc-nscd-source-closure.json new file mode 100644 index 0000000000..812a4989a9 --- /dev/null +++ b/server/security/ubuntu-backports/bundle/glibc-nscd-source-closure.json @@ -0,0 +1,520 @@ +{ + "audit": { + "buildObjectClosure": { + "allNativeLinkCommandsMentioningTheseObjectsTargetOnlyNscd": true, + "buildNscd": "yes", + "installTemplate": "debian/debhelper.in/nscd.install", + "libcClientRoutinesAreTheVulnerableDaemonModules": false, + "libcClientRoutinesRemain": [ + "nscd_getpw_r", + "nscd_getgr_r", + "nscd_gethst_r", + "nscd_getai", + "nscd_initgroups", + "nscd_getserv_r", + "nscd_netgroup", + "nscd_helper" + ], + "nativePackagingTrace": [ + { + "line": 131850, + "text": "dh_install -pnscd" + }, + { + "line": 131930, + "text": "dpkg-deb: building package 'nscd' in '../nscd_2.43-2ubuntu2.4+pasturestack.1_amd64.deb'." + } + ], + "nscdInstallTarget": "usr/sbin/nscd", + "objectToExecutable": [ + { + "line": 21574, + "text": "x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS -Wl,-rpath-link=/work/native/glibc-source/build-tree/amd64-libc:/work/native/glibc-source/build-tree/amd64-libc/math:/work/native/glibc-source/build-tree/amd64-libc/elf:/work/native/glibc-source/build-tree/amd64-libc/dlfcn:/work/native/glibc-source/build-tree/amd64-libc/nss:/work/native/glibc-source/build-tree/amd64-libc/nis:/work/native/glibc-source/build-tree/amd64-libc/rt:/work/native/glibc-source/build-tree/amd64-libc/resolv:/work/native/glibc-source/build-tree/amd64-libc/mathvec:/work/native/glibc-source/build-tree/amd64-libc/support:/work/native/glibc-source/build-tree/amd64-libc/misc:/work/native/glibc-source/build-tree/amd64-libc/debug:/work/native/glibc-source/build-tree/amd64-libc/nptl:/usr/lib/gcc/x86_64-linux-gnu/15/../../../x86_64-linux-gnu/ -o /work/native/glibc-source/build-tree/amd64-libc/nscd/nscd -pie -Wl,-z,pack-relative-relocs -Wl,-O1 -nostdlib -nostartfiles -Wl,-z,mark-plt -Wl,-z,now -Wl,-z,relro /work/native/glibc-source/build-tree/amd64-libc/csu/Scrt1.o /work/native/glibc-source/build-tree/amd64-libc/csu/crti.o `x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS -Wl,-z,mark-plt --print-file-name=crtbeginS.o` /work/native/glibc-source/build-tree/amd64-libc/nscd/nscd.o /work/native/glibc-source/build-tree/amd64-libc/nscd/connections.o /work/native/glibc-source/build-tree/amd64-libc/nscd/pwdcache.o /work/native/glibc-source/build-tree/amd64-libc/nscd/getpwnam_r.o /work/native/glibc-source/build-tree/amd64-libc/nscd/getpwuid_r.o /work/native/glibc-source/build-tree/amd64-libc/nscd/grpcache.o /work/native/glibc-source/build-tree/amd64-libc/nscd/getgrnam_r.o /work/native/glibc-source/build-tree/amd64-libc/nscd/getgrgid_r.o /work/native/glibc-source/build-tree/amd64-libc/nscd/hstcache.o /work/native/glibc-source/build-tree/amd64-libc/nscd/gethstbyad_r.o /work/native/glibc-source/build-tree/amd64-libc/nscd/gethstbynm3_r.o /work/native/glibc-source/build-tree/amd64-libc/nscd/getsrvbynm_r.o /work/native/glibc-source/build-tree/amd64-libc/nscd/getsrvbypt_r.o /work/native/glibc-source/build-tree/amd64-libc/nscd/servicescache.o /work/native/glibc-source/build-tree/amd64-libc/nscd/dbg_log.o /work/native/glibc-source/build-tree/amd64-libc/nscd/nscd_conf.o /work/native/glibc-source/build-tree/amd64-libc/nscd/nscd_stat.o /work/native/glibc-source/build-tree/amd64-libc/nscd/cache.o /work/native/glibc-source/build-tree/amd64-libc/nscd/mem.o /work/native/glibc-source/build-tree/amd64-libc/nscd/nscd_setup_thread.o /work/native/glibc-source/build-tree/amd64-libc/nscd/xmalloc.o /work/native/glibc-source/build-tree/amd64-libc/nscd/xstrdup.o /work/native/glibc-source/build-tree/amd64-libc/nscd/aicache.o /work/native/glibc-source/build-tree/amd64-libc/nscd/initgrcache.o /work/native/glibc-source/build-tree/amd64-libc/nscd/res_hconf.o /work/native/glibc-source/build-tree/amd64-libc/nscd/netgroupcache.o /work/native/glibc-source/build-tree/amd64-libc/nscd/cachedumper.o /work/native/glibc-source/build-tree/amd64-libc/nscd/selinux.o -lselinux -laudit -lcap -Wl,-dynamic-linker=/lib64/ld-linux-x86-64.so.2 -Wl,-z,now /work/native/glibc-source/build-tree/amd64-libc/libc.so.6 /work/native/glibc-source/build-tree/amd64-libc/libc_nonshared.a -Wl,--as-needed /work/native/glibc-source/build-tree/amd64-libc/elf/ld.so -Wl,--no-as-needed -lgcc `x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS -Wl,-z,mark-plt --print-file-name=crtendS.o` /work/native/glibc-source/build-tree/amd64-libc/csu/crtn.o" + }, + { + "line": 131046, + "text": "x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS -Wl,-rpath-link=/work/native/glibc-source/build-tree/amd64-C.utf8:/work/native/glibc-source/build-tree/amd64-C.utf8/math:/work/native/glibc-source/build-tree/amd64-C.utf8/elf:/work/native/glibc-source/build-tree/amd64-C.utf8/dlfcn:/work/native/glibc-source/build-tree/amd64-C.utf8/nss:/work/native/glibc-source/build-tree/amd64-C.utf8/nis:/work/native/glibc-source/build-tree/amd64-C.utf8/rt:/work/native/glibc-source/build-tree/amd64-C.utf8/resolv:/work/native/glibc-source/build-tree/amd64-C.utf8/mathvec:/work/native/glibc-source/build-tree/amd64-C.utf8/support:/work/native/glibc-source/build-tree/amd64-C.utf8/misc:/work/native/glibc-source/build-tree/amd64-C.utf8/debug:/work/native/glibc-source/build-tree/amd64-C.utf8/nptl:/usr/lib/gcc/x86_64-linux-gnu/15/../../../x86_64-linux-gnu/ -o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/nscd -pie -Wl,-z,pack-relative-relocs -Wl,-O1 -nostdlib -nostartfiles -Wl,-z,mark-plt -Wl,-z,now -Wl,-z,relro /work/native/glibc-source/build-tree/amd64-C.utf8/csu/Scrt1.o /work/native/glibc-source/build-tree/amd64-C.utf8/csu/crti.o `x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS -Wl,-z,mark-plt --print-file-name=crtbeginS.o` /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/nscd.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/connections.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/pwdcache.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/getpwnam_r.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/getpwuid_r.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/grpcache.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/getgrnam_r.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/getgrgid_r.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/hstcache.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/gethstbyad_r.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/gethstbynm3_r.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/getsrvbynm_r.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/getsrvbypt_r.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/servicescache.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/dbg_log.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/nscd_conf.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/nscd_stat.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/cache.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/mem.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/nscd_setup_thread.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/xmalloc.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/xstrdup.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/aicache.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/initgrcache.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/res_hconf.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/netgroupcache.o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/cachedumper.o -Wl,-dynamic-linker=/lib/ld-linux-x86-64.so.2 -Wl,-z,now /work/native/glibc-source/build-tree/amd64-C.utf8/libc.so.6 /work/native/glibc-source/build-tree/amd64-C.utf8/libc_nonshared.a -Wl,--as-needed /work/native/glibc-source/build-tree/amd64-C.utf8/elf/ld.so -Wl,--no-as-needed -lgcc `x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS -Wl,-z,mark-plt --print-file-name=crtendS.o` /work/native/glibc-source/build-tree/amd64-C.utf8/csu/crtn.o" + } + ], + "sourceToObject": [ + { + "line": 21554, + "text": "x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS hstcache.c -c -std=gnu11 -fgnu89-inline -pipe -O2 -g -fdebug-prefix-map=/work/native/glibc-source=. -O3 -Wall -Wwrite-strings -Wundef -Wimplicit-fallthrough -Werror -fmerge-all-constants -frounding-math -ftrapping-math -fstack-protector-strong -fno-common -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -Wstrict-prototypes -Wold-style-definition -Wfree-labels -Wmissing-parameter-name -fmath-errno -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -DHAVE_EPOLL -DHAVE_INOTIFY -DHAVE_NETLINK -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fPIE -fcf-protection -fPIE -isystem /work/native/glibc-source/debian/include -I../include -I/work/native/glibc-source/build-tree/amd64-libc/nscd -I/work/native/glibc-source/build-tree/amd64-libc -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/x86_64/64/nptl -I../sysdeps/unix/sysv/linux/x86_64/include -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86/include -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -nostdinc -isystem /usr/lib/gcc/x86_64-linux-gnu/15/include -isystem /work/native/glibc-source/debian/include -D_LIBC_REENTRANT -include /work/native/glibc-source/build-tree/amd64-libc/libc-modules.h -DMODULE_NAME=nscd -include ../include/libc-symbols.h -DPIC -DTOP_NAMESPACE=glibc -o /work/native/glibc-source/build-tree/amd64-libc/nscd/hstcache.o -MD -MP -MF /work/native/glibc-source/build-tree/amd64-libc/nscd/hstcache.o.dt -MT /work/native/glibc-source/build-tree/amd64-libc/nscd/hstcache.o" + }, + { + "line": 21568, + "text": "x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS aicache.c -c -std=gnu11 -fgnu89-inline -pipe -O2 -g -fdebug-prefix-map=/work/native/glibc-source=. -O3 -Wall -Wwrite-strings -Wundef -Wimplicit-fallthrough -Werror -fmerge-all-constants -frounding-math -ftrapping-math -fstack-protector-strong -fno-common -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -Wstrict-prototypes -Wold-style-definition -Wfree-labels -Wmissing-parameter-name -fmath-errno -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -DHAVE_EPOLL -DHAVE_INOTIFY -DHAVE_NETLINK -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fPIE -fcf-protection -fPIE -isystem /work/native/glibc-source/debian/include -I../include -I/work/native/glibc-source/build-tree/amd64-libc/nscd -I/work/native/glibc-source/build-tree/amd64-libc -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/x86_64/64/nptl -I../sysdeps/unix/sysv/linux/x86_64/include -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86/include -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -nostdinc -isystem /usr/lib/gcc/x86_64-linux-gnu/15/include -isystem /work/native/glibc-source/debian/include -D_LIBC_REENTRANT -include /work/native/glibc-source/build-tree/amd64-libc/libc-modules.h -DMODULE_NAME=nscd -include ../include/libc-symbols.h -DPIC -DTOP_NAMESPACE=glibc -o /work/native/glibc-source/build-tree/amd64-libc/nscd/aicache.o -MD -MP -MF /work/native/glibc-source/build-tree/amd64-libc/nscd/aicache.o.dt -MT /work/native/glibc-source/build-tree/amd64-libc/nscd/aicache.o" + }, + { + "line": 131027, + "text": "x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS hstcache.c -c -std=gnu11 -fgnu89-inline -pipe -O2 -g -fdebug-prefix-map=/work/native/glibc-source=. -Wall -Wwrite-strings -Wundef -Wimplicit-fallthrough -Werror -fmerge-all-constants -frounding-math -ftrapping-math -fstack-protector-strong -fno-common -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -Wstrict-prototypes -Wold-style-definition -Wfree-labels -Wmissing-parameter-name -fmath-errno -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -DHAVE_EPOLL -DHAVE_INOTIFY -DHAVE_NETLINK -fPIE -fcf-protection -fPIE -isystem /work/native/glibc-source/debian/include -I../include -I/work/native/glibc-source/build-tree/amd64-C.utf8/nscd -I/work/native/glibc-source/build-tree/amd64-C.utf8 -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/x86_64/64/nptl -I../sysdeps/unix/sysv/linux/x86_64/include -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86/include -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -nostdinc -isystem /usr/lib/gcc/x86_64-linux-gnu/15/include -isystem /work/native/glibc-source/debian/include -D_LIBC_REENTRANT -include /work/native/glibc-source/build-tree/amd64-C.utf8/libc-modules.h -DMODULE_NAME=nscd -include ../include/libc-symbols.h -DPIC -DTOP_NAMESPACE=glibc -o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/hstcache.o -MD -MP -MF /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/hstcache.o.dt -MT /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/hstcache.o" + }, + { + "line": 131041, + "text": "x86_64-linux-gnu-gcc -Wl,--hash-style=both -U_FILE_OFFSET_BITS -U_TIME_BITS aicache.c -c -std=gnu11 -fgnu89-inline -pipe -O2 -g -fdebug-prefix-map=/work/native/glibc-source=. -Wall -Wwrite-strings -Wundef -Wimplicit-fallthrough -Werror -fmerge-all-constants -frounding-math -ftrapping-math -fstack-protector-strong -fno-common -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -Wstrict-prototypes -Wold-style-definition -Wfree-labels -Wmissing-parameter-name -fmath-errno -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -DHAVE_EPOLL -DHAVE_INOTIFY -DHAVE_NETLINK -fPIE -fcf-protection -fPIE -isystem /work/native/glibc-source/debian/include -I../include -I/work/native/glibc-source/build-tree/amd64-C.utf8/nscd -I/work/native/glibc-source/build-tree/amd64-C.utf8 -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/x86_64/64/nptl -I../sysdeps/unix/sysv/linux/x86_64/include -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86/include -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -nostdinc -isystem /usr/lib/gcc/x86_64-linux-gnu/15/include -isystem /work/native/glibc-source/debian/include -D_LIBC_REENTRANT -include /work/native/glibc-source/build-tree/amd64-C.utf8/libc-modules.h -DMODULE_NAME=nscd -include ../include/libc-symbols.h -DPIC -DTOP_NAMESPACE=glibc -o /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/aicache.o -MD -MP -MF /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/aicache.o.dt -MT /work/native/glibc-source/build-tree/amd64-C.utf8/nscd/aicache.o" + } + ], + "useNscd": "yes" + }, + "cve": "CVE-2026-89092", + "effectiveSourceClosure": { + "allActivePatchesOldAndNewTargetsParsed": true, + "firstPartyPatchTargetClosure": [ + { + "commit": "72351055c6a1328c1ce2c06b72b8a4e3ebe0ef08", + "selectedTargetHits": [], + "sha256": "73ca6cb57d78c89785cd2bdd5c15cf7e5fdee873599bb8f1623035a2692915f1", + "targets": [ + "libio/fileops.c" + ] + }, + { + "commit": "20b3e8717652bdc2f89c99c4cc6e38d56a754866", + "selectedTargetHits": [], + "sha256": "bf4de5ac1937b76fd21ce003c77f5681b899edb8a42326ff3a13bf1ba4ab4ba1", + "targets": [ + "libio/Makefile", + "libio/tst-fopen-ccs-empty.c" + ] + } + ], + "official243Patch": { + "bodySha256": "cf5e2f6281a5b8668edd8cdf8dc9779614d487beab762077098afe4ecb0c1ba3", + "changedPaths": [ + "nscd/aicache.c", + "nscd/hstcache.c" + ], + "commit": "e51efbc44f228425a405b805637d80ef81dc2345", + "htmlSha256": "9e0aafecad887ba3b7c29c5d2e6ff31b0787729ce841a719aa5760e2fa120a72", + "messageId": "<20260929091145.B8AE54BB24CC@sourceware.org>", + "title": "[glibc/release/2.43/master] CVE-2026-89092: nscd: replace alloca with malloc in aicache, hstcache", + "transport": "Direct official Sourceware glibc-cvs archive", + "url": "https://sourceware.org/pipermail/glibc-cvs/2026q3/091543.html" + }, + "originalUbuntuActivePatchCount": 103, + "packagingRoles": [ + { + "path": "debian/debhelper.in/nscd.install", + "sameOriginalAndNativeApprovedPackaging": true, + "sha256": "b5d96bfde599ef00c7a1340619d1ac3fb6cbe2b21c180ae7f964435aa7076d70" + }, + { + "path": "debian/debhelper.in/libc.install", + "sameOriginalAndNativeApprovedPackaging": true, + "sha256": "6ec02d6eb79827861c632d5e93052fa3a5e3e936e65fd05b3ac40516104df773" + }, + { + "path": "debian/debhelper.in/libc-bin.install", + "sameOriginalAndNativeApprovedPackaging": true, + "sha256": "8550a9785a6622b366405424ebd9a104e96e9ee55f693d2298a72e6ae6afe3d6" + }, + { + "path": "debian/rules.d/build.mk", + "sameOriginalAndNativeApprovedPackaging": true, + "sha256": "26c43dbea2c6297b1db868a10b982091df054545c7e1f8765a8d622c78668b2f" + }, + { + "path": "debian/rules.d/debhelper.mk", + "sameOriginalAndNativeApprovedPackaging": true, + "sha256": "a361f9db42b6881b62b557ecb00dd55b12d53598511dbb502035a4d7329ba3a4" + } + ], + "patchParserSemantics": "Parse both old/new unified/deletion headers with horizontal whitespace only; ignore mail separators/diffstat; all 103 active original patches use default -p1, every patch has recognized file targets.", + "selectedEffectiveSource": [ + { + "derivation": "No active original quilt old/new target and no approved first-party patch targets this file; effective bytes exactly equal pinned orig source without applying unrelated patches.", + "effectiveNativeSourceSha256": "d4c483f617e10e191857345fa6dfc2a37726261eaf0fd5d01ff59d7057e829cf", + "firstPartyPatchHits": [], + "originalSha256": "d4c483f617e10e191857345fa6dfc2a37726261eaf0fd5d01ff59d7057e829cf", + "originalUbuntuPatchHits": [], + "path": "nscd/Makefile" + }, + { + "derivation": "No active original quilt old/new target and no approved first-party patch targets this file; effective bytes exactly equal pinned orig source without applying unrelated patches.", + "effectiveNativeSourceSha256": "36666de22337bd4434c2fb7140d4ea77880afef2fdd5cbd06dc36b7463a579d7", + "firstPartyPatchHits": [], + "originalSha256": "36666de22337bd4434c2fb7140d4ea77880afef2fdd5cbd06dc36b7463a579d7", + "originalUbuntuPatchHits": [], + "path": "nscd/aicache.c" + }, + { + "derivation": "No active original quilt old/new target and no approved first-party patch targets this file; effective bytes exactly equal pinned orig source without applying unrelated patches.", + "effectiveNativeSourceSha256": "93a2f56daeca0887ba1ceef35bc722cedfd7a025c85a1eed54e7da4cd0111f23", + "firstPartyPatchHits": [], + "originalSha256": "93a2f56daeca0887ba1ceef35bc722cedfd7a025c85a1eed54e7da4cd0111f23", + "originalUbuntuPatchHits": [], + "path": "nscd/hstcache.c" + } + ], + "status": "validated" + }, + "excludedNscdPackage": { + "archiveIndependentlyReviewed": false, + "assessment": "Outside this code-absence conclusion. The independent nscd daemon retains vulnerable code and needs its own finding/fix if installed.", + "builtAccordingToNativeChanges": true, + "changesDeclaredBytes": 97950, + "changesDeclaredFile": "nscd_2.43-2ubuntu2.4+pasturestack.1_amd64.deb", + "changesDeclaredSha256": "8597c0678d7dd5a15e1b6263c6e469aa6d2768c2dbb99a5b084a0def587c6052", + "name": "nscd", + "qualifiedExport": false, + "version": "2.43-2ubuntu2.4+pasturestack.1" + }, + "limitations": [ + "The source adds only CVE-2026-18374 fix/test backports; it does not fix CVE-2026-89092 in nscd.", + "libc6 retains nscd IPC client helpers. Only aicache/hstcache daemon modules are absent from these three runtime packages.", + "A separate nscd package or unmanaged nscd binary must not inherit these exact-PURL statements.", + "The selected18 existing SBOM package checks supplement source/object/archive closure; they are not new rootfs scans or whole-consumer not-affected proof.", + "No native compilation, upstream suite, runtime test, ledger edit or VEX publication was performed for this source-absence audit." + ], + "nativeArtifact": { + "provenanceMember": "export/package/provenance.json", + "scope": "Existing native artifact identity; no claim of a new bundle publication.", + "url": "https://github.com/PastureStack/server/actions/runs/40122645083/artifacts/12573643405", + "zipSha256": "ed6714b9b4a884a0c0462b84b01f5d7f9e08b4cc1bbf849b521ac43fa4ef36c4" + }, + "nativeIdentity": { + "apiReceiptSha256": "5ebd5e8502de1b4caee1df8e3af6249a5e234b0ecf54a1012d091f777ca3e126", + "artifactId": 12573643405, + "artifactName": "ubuntu-native-glibc-507cadcb3eaafcd6a85c453f2f180a8aef415d88-40122645083", + "artifactZipSha256": "ed6714b9b4a884a0c0462b84b01f5d7f9e08b4cc1bbf849b521ac43fa4ef36c4", + "buildCommand": [ + "dpkg-buildpackage", + "-G", + "-us", + "-uc", + "-Pnobiarch,noudeb", + "-j4" + ], + "buildLogBytes": 86607135, + "buildLogSha256": "a6f1ec5c9f303a4bea24b22d5d7ce14fe68a739b3ab0cc77bad075c88d5d8f78", + "jobId": 120957337330, + "provenanceSha256": "cc77cdde3f1b6b5459b45282bbbdcccbe42c3d3fd5d7a41f4623a92509a2263a", + "runId": 40122645083, + "sourceCommit": "507cadcb3eaafcd6a85c453f2f180a8aef415d88", + "sourceLockSha256": "5b4ead11dcace84b99874d298eec4d6939203040aecd5cfc3f4452a83eb0bf12", + "sourceRecipeFiles": { + "server/security/ubuntu-backports/build-pcre2-backport.py": "79ff6faf3ca5b8e6dfdfc38a78ba56238c749aeabfab59fd05ce2130537458b2", + "server/security/ubuntu-backports/glibc/build-glibc-backport.py": "a086f51359bab0d0994070d4cdf0e4fcc1f19bc4acf8dbd71d189707c72ef3e6", + "server/security/ubuntu-backports/glibc/glibc.lock.json": "5b4ead11dcace84b99874d298eec4d6939203040aecd5cfc3f4452a83eb0bf12", + "server/security/ubuntu-backports/glibc/test_glibc_backport.py": "d08cb0b93d6c49c0124eb69cedd55af3949b41c48a09c57d52d49e304641a583" + } + }, + "packages": [ + { + "architecture": "amd64", + "archive": { + "bytes": 2104192, + "file": "libc6_2.43-2ubuntu2.4+pasturestack.1_amd64.deb", + "sha256": "4119ebc8610d17694db4100e6838a693f8927189523f4b4be32be5aa4a82f497" + }, + "assessment": { + "justification": "vulnerable_code_not_present", + "scope": "Only the two CVE-2026-89092 daemon implementations; not all nscd code or the whole consumer.", + "status": "not_affected" + }, + "codeAbsenceScope": [ + "nscd/aicache.c", + "nscd/hstcache.c" + ], + "control": { + "metadata": { + "Architecture": "amd64", + "Package": "libc6", + "Source": "glibc", + "Version": "2.43-2ubuntu2.4+pasturestack.1" + }, + "sha256": "fe0d96aa87e1043f2c0346cd7aebe6664c389ff430cce8532ffbd1b9e3bea802" + }, + "dataArchive": { + "bytes": 2045238, + "hashMeaning": "Exact compressed ar member bytes; not uncompressed tar bytes", + "member": "data.tar.zst", + "sha256": "04e312bab6e6838012c05b0c01f8e2998acff37e8600727bdb78dcd8283dc756" + }, + "dataArchiveListing": { + "matchesQualifierPayloadInventory": true, + "nonDirectoryFileCount": 40, + "nscdDaemonPathMatches": [], + "sha256": "2fcadc9a09df10dc1453ff54e64b20f048a672df3b849c2bae3a657dc7fdf903" + }, + "name": "libc6", + "purl": "pkg:deb/ubuntu/libc6@2.43-2ubuntu2.4%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04", + "qualifierPayload": { + "elfCount": 28, + "fileCount": 40, + "hashMeaning": "Original qualified payload JSON bytes, distinct from data archive/listing hashes", + "path": "payload/libc6.json", + "sha256": "3468d23e1ec411706726c09fea81158c36be984cbc7b90c7a95950ca76e32c27" + }, + "source": "glibc", + "version": "2.43-2ubuntu2.4+pasturestack.1" + }, + { + "architecture": "amd64", + "archive": { + "bytes": 724758, + "file": "libc-bin_2.43-2ubuntu2.4+pasturestack.1_amd64.deb", + "sha256": "6e8ea949edd167a48e4f2ded82f4117e4dca89fc7c8060062b614bbb682b9417" + }, + "assessment": { + "justification": "vulnerable_code_not_present", + "scope": "Only the two CVE-2026-89092 daemon implementations; not all nscd code or the whole consumer.", + "status": "not_affected" + }, + "codeAbsenceScope": [ + "nscd/aicache.c", + "nscd/hstcache.c" + ], + "control": { + "metadata": { + "Architecture": "amd64", + "Package": "libc-bin", + "Source": "glibc", + "Version": "2.43-2ubuntu2.4+pasturestack.1" + }, + "sha256": "310444de66f2bdf6c9eedb1c7db87ca15ada0cbbc7c2084e31048f6574cebaf1" + }, + "dataArchive": { + "bytes": 722278, + "hashMeaning": "Exact compressed ar member bytes; not uncompressed tar bytes", + "member": "data.tar.zst", + "sha256": "f944912c88cc7d7241d4f76f82250d57f1e6f9eac8b9e95551deedd8b1d42987" + }, + "dataArchiveListing": { + "matchesQualifierPayloadInventory": true, + "nonDirectoryFileCount": 36, + "nscdDaemonPathMatches": [], + "sha256": "c31a54190fa2f60a1f9d5e631d26f198707b881d8267d0772d0bbe6b21ecace6" + }, + "name": "libc-bin", + "purl": "pkg:deb/ubuntu/libc-bin@2.43-2ubuntu2.4%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04", + "qualifierPayload": { + "elfCount": 10, + "fileCount": 36, + "hashMeaning": "Original qualified payload JSON bytes, distinct from data archive/listing hashes", + "path": "payload/libc-bin.json", + "sha256": "a09513fcff9ba63ef0c8914fc195c375b2073433e870cc45d732d0fe9ac6ca91" + }, + "source": "glibc", + "version": "2.43-2ubuntu2.4+pasturestack.1" + }, + { + "architecture": "amd64", + "archive": { + "bytes": 1362648, + "file": "libc-gconv-modules-extra_2.43-2ubuntu2.4+pasturestack.1_amd64.deb", + "sha256": "22cb6a83267d6e873538983d1eafda6cac9240c5c18ff61e2652fd6ae982e374" + }, + "assessment": { + "justification": "vulnerable_code_not_present", + "scope": "Only the two CVE-2026-89092 daemon implementations; not all nscd code or the whole consumer.", + "status": "not_affected" + }, + "codeAbsenceScope": [ + "nscd/aicache.c", + "nscd/hstcache.c" + ], + "control": { + "metadata": { + "Architecture": "amd64", + "Package": "libc-gconv-modules-extra", + "Source": "glibc", + "Version": "2.43-2ubuntu2.4+pasturestack.1" + }, + "sha256": "e238b331655c3038764e7c8a8b41e47dab9354feaa6cd9e2a2edab85c8a26871" + }, + "dataArchive": { + "bytes": 1356221, + "hashMeaning": "Exact compressed ar member bytes; not uncompressed tar bytes", + "member": "data.tar.zst", + "sha256": "3a134f4e87d50a22d9152ffba4fe370f10dcebfdfb19ac630b1972e43d37e1ec" + }, + "dataArchiveListing": { + "matchesQualifierPayloadInventory": true, + "nonDirectoryFileCount": 250, + "nscdDaemonPathMatches": [], + "sha256": "8127c706ee3f1ab80692978775b5c9ed3561615f7f6be3f3090db90cf0971fdc" + }, + "name": "libc-gconv-modules-extra", + "purl": "pkg:deb/ubuntu/libc-gconv-modules-extra@2.43-2ubuntu2.4%2Bpasturestack.1?arch=amd64&distro=ubuntu-26.04", + "qualifierPayload": { + "elfCount": 245, + "fileCount": 250, + "hashMeaning": "Original qualified payload JSON bytes, distinct from data archive/listing hashes", + "path": "payload/libc-gconv-modules-extra.json", + "sha256": "5f6a90c7c87bc723bf8009b9699b8dd0df1aad31f0f19b5d735595874e1e00b4" + }, + "source": "glibc", + "version": "2.43-2ubuntu2.4+pasturestack.1" + } + ], + "schemaVersion": 1, + "sourceIdentity": { + "customVersion": "2.43-2ubuntu2.4+pasturestack.1", + "firstPartyFixes": [ + "CVE-2026-18374" + ], + "sourceFiles": [ + { + "name": "glibc_2.43-2ubuntu2.4.dsc", + "sha256": "da10d551ca51076bf7bf663de38369d04e267386b8211c283c2814e68e482ed3", + "size": 9078, + "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43-2ubuntu2.4.dsc" + }, + { + "name": "glibc_2.43.orig.tar.xz", + "sha256": "d9c86c6b5dbddb43a3e08270c5844fc5177d19442cf5b8df4be7c07cd5fa3831", + "size": 20297012, + "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43.orig.tar.xz" + }, + { + "name": "glibc_2.43.orig.tar.xz.asc", + "sha256": "0a725eb20ceb526abf52d3129f03e0399afa0d97ffa1dd779bed42958ab23326", + "size": 1018, + "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43.orig.tar.xz.asc" + }, + { + "name": "glibc_2.43-2ubuntu2.4.debian.tar.xz", + "sha256": "28103a7cf808c29901c6053d89a4e8299880abfbb850c0d97a09c8df8533e306", + "size": 530764, + "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43-2ubuntu2.4.debian.tar.xz" + } + ], + "sourceLockSha256": "5b4ead11dcace84b99874d298eec4d6939203040aecd5cfc3f4452a83eb0bf12", + "sourcePackage": "glibc", + "sourcePublication": "https://api.launchpad.net/1.0/ubuntu/+archive/primary/+sourcepub/18721986", + "ubuntuSourceVersion": "2.43-2ubuntu2.4" + }, + "status": "qualified", + "upstreamProof": { + "advisory": { + "sha256": "e5cb1a5e3a1281e0f5e91d7cee9248d4545c5a0594b78b566bcce845531adf11", + "url": "https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=advisories/GLIBC-SA-2026-0016" + }, + "mirrorTransportUsedAsAuthority": false, + "official243Mail": { + "bodySha256": "cf5e2f6281a5b8668edd8cdf8dc9779614d487beab762077098afe4ecb0c1ba3", + "changedPaths": [ + "nscd/aicache.c", + "nscd/hstcache.c" + ], + "commit": "e51efbc44f228425a405b805637d80ef81dc2345", + "htmlSha256": "9e0aafecad887ba3b7c29c5d2e6ff31b0787729ce841a719aa5760e2fa120a72", + "messageId": "<20260929091145.B8AE54BB24CC@sourceware.org>", + "title": "[glibc/release/2.43/master] CVE-2026-89092: nscd: replace alloca with malloc in aicache, hstcache", + "transport": "Direct official Sourceware glibc-cvs archive", + "url": "https://sourceware.org/pipermail/glibc-cvs/2026q3/091543.html" + }, + "vulnerableModules": [ + "nscd/aicache.c", + "nscd/hstcache.c" + ] + } + }, + "auditProjectionSha256": "3676a3e8c6f9ea3385608dc81e27280749fbdbc1f874d9ca9a994979cb98ae9b", + "cve": "CVE-2026-89092", + "debs": { + "libc-bin": { + "file": "libc-bin_2.43-2ubuntu2.4+pasturestack.1_amd64.deb", + "sha256": "6e8ea949edd167a48e4f2ded82f4117e4dca89fc7c8060062b614bbb682b9417" + }, + "libc-gconv-modules-extra": { + "file": "libc-gconv-modules-extra_2.43-2ubuntu2.4+pasturestack.1_amd64.deb", + "sha256": "22cb6a83267d6e873538983d1eafda6cac9240c5c18ff61e2652fd6ae982e374" + }, + "libc6": { + "file": "libc6_2.43-2ubuntu2.4+pasturestack.1_amd64.deb", + "sha256": "4119ebc8610d17694db4100e6838a693f8927189523f4b4be32be5aa4a82f497" + } + }, + "detachedAuditId": "glibc-CVE-2026-89092-runtime-code-absence-amd64", + "nativeIdentity": { + "artifactId": 12573643405, + "jobId": 120957337330, + "lockSha256": "5b4ead11dcace84b99874d298eec4d6939203040aecd5cfc3f4452a83eb0bf12", + "provenanceSha256": "cc77cdde3f1b6b5459b45282bbbdcccbe42c3d3fd5d7a41f4623a92509a2263a", + "runId": 40122645083, + "sourceCommit": "507cadcb3eaafcd6a85c453f2f180a8aef415d88", + "zipSha256": "ed6714b9b4a884a0c0462b84b01f5d7f9e08b4cc1bbf849b521ac43fa4ef36c4" + }, + "originalReceiptSha256": "d6fe389d88eb9d32dca81c8408b53647dae52a6f2d3f9669dac1b74271bd9f46", + "platform": { + "architecture": "amd64", + "distribution": "ubuntu", + "release": "26.04" + }, + "projection": { + "detachedAuditReference": "The original audit hash/id is detached provenance, not a downloadable file reference.", + "excludedFields": [ + "productInventoryCrosscheck", + "qualifiedFamilyCrosscheck", + "evidence", + "complete-native-build-log", + "native-archives-and-full-inventories" + ], + "scope": "Only source/object/exact-three-runtime-package evidence is delivered here." + }, + "schemaVersion": 1, + "scope": { + "customVersion": "2.43-2ubuntu2.4+pasturestack.1", + "libcNscdIpcClientsMayBePresent": true, + "runtimePackages": [ + "libc-bin", + "libc-gconv-modules-extra", + "libc6" + ], + "standaloneDaemonPackage": "nscd", + "verdict": "component-code-absence-only", + "vulnerableSources": [ + "nscd/aicache.c", + "nscd/hstcache.c" + ], + "wholeImageUnaffectedClaimed": false + }, + "sourceIdentity": { + "patches": [ + { + "commit": "72351055c6a1328c1ce2c06b72b8a4e3ebe0ef08", + "name": "72351055c6a1328c1ce2c06b72b8a4e3ebe0ef08.sourceware.patch", + "sha256": "73ca6cb57d78c89785cd2bdd5c15cf7e5fdee873599bb8f1623035a2692915f1", + "size": 2175, + "url": "https://sourceware.org/git/?p=glibc.git;a=patch;h=72351055c6a1328c1ce2c06b72b8a4e3ebe0ef08" + }, + { + "commit": "20b3e8717652bdc2f89c99c4cc6e38d56a754866", + "name": "20b3e8717652bdc2f89c99c4cc6e38d56a754866.sourceware.patch", + "sha256": "bf4de5ac1937b76fd21ce003c77f5681b899edb8a42326ff3a13bf1ba4ab4ba1", + "size": 3183, + "url": "https://sourceware.org/git/?p=glibc.git;a=patch;h=20b3e8717652bdc2f89c99c4cc6e38d56a754866" + } + ], + "sourceFiles": [ + { + "name": "glibc_2.43-2ubuntu2.4.dsc", + "sha256": "da10d551ca51076bf7bf663de38369d04e267386b8211c283c2814e68e482ed3", + "size": 9078, + "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43-2ubuntu2.4.dsc" + }, + { + "name": "glibc_2.43.orig.tar.xz", + "sha256": "d9c86c6b5dbddb43a3e08270c5844fc5177d19442cf5b8df4be7c07cd5fa3831", + "size": 20297012, + "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43.orig.tar.xz" + }, + { + "name": "glibc_2.43.orig.tar.xz.asc", + "sha256": "0a725eb20ceb526abf52d3129f03e0399afa0d97ffa1dd779bed42958ab23326", + "size": 1018, + "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43.orig.tar.xz.asc" + }, + { + "name": "glibc_2.43-2ubuntu2.4.debian.tar.xz", + "sha256": "28103a7cf808c29901c6053d89a4e8299880abfbb850c0d97a09c8df8533e306", + "size": 530764, + "url": "https://archive.ubuntu.com/ubuntu/pool/main/g/glibc/glibc_2.43-2ubuntu2.4.debian.tar.xz" + } + ], + "ubuntuSourceVersion": "2.43-2ubuntu2.4" + }, + "status": "validated" +} diff --git a/server/security/ubuntu-backports/bundle/qualify-native-packages.py b/server/security/ubuntu-backports/bundle/qualify-native-packages.py index 47b8384848..4b722d85a3 100644 --- a/server/security/ubuntu-backports/bundle/qualify-native-packages.py +++ b/server/security/ubuntu-backports/bundle/qualify-native-packages.py @@ -31,6 +31,10 @@ LOCK_FILE = HERE / 'source-inputs.lock.json' RUST_AUDIT_FILE = HERE / 'rust-coreutils-original18-source-closure.json' RUST_AUDIT_RECEIPT_SHA = '73189028622c0a0fc074067b4c5b8521094921531f95cf3e68073baaebd2fbbd' +GLIBC_AUDIT_FILE = HERE / 'glibc-nscd-source-closure.json' +GUARD_SPEC = importlib.util.spec_from_file_location('backport_metadata_guard', HERE / 'verify-consumer-evidence.py') +GUARD = importlib.util.module_from_spec(GUARD_SPEC) +GUARD_SPEC.loader.exec_module(GUARD) WORK = Path(tempfile.gettempdir()) / 'pasturestack-ubuntu-security-backports-v0.1.0' REPO = 'PastureStack/server' PLATFORM = {'distribution': 'ubuntu', 'release': '26.04', 'architecture': 'amd64'} @@ -153,9 +157,52 @@ def load_lock(ready=False): and safe_name(p['file']).name == p['file'] and re.fullmatch('[0-9a-f]{64}', p['sha256']), 'Unexpected pinned PCRE2 package') load_rust_source_audit(lock['nativeInputs']['rust-coreutils']) + require(all(GUARD.GLIBC_NSCD_CVE not in item['fixedVulnerabilities'] + for item in [lock['pcre2'], *lock['nativeInputs'].values()]), + 'nscd code absence must never be recorded as a fixed vulnerability') + if lock['nativeInputs']['glibc']['status'] != 'pending': + load_glibc_nscd_source_audit(lock['nativeInputs']['glibc'], ready=ready) return lock +def load_glibc_nscd_source_audit(item, ready=False): + ref = item.get('codeAbsenceProof', {}) + require(ref.get('path') == GLIBC_AUDIT_FILE.name and file_sha(GLIBC_AUDIT_FILE) == ref.get('sha256'), + 'glibc nscd public source proof is missing or changed') + public = json.loads(GLIBC_AUDIT_FILE.read_text(encoding='utf-8')) + if public.get('status') == 'pending-source-closure': + require(GUARD.GLIBC_NSCD_PROOF_SHA256 is None and public['cve'] == GUARD.GLIBC_NSCD_CVE + and public['platform'] == PLATFORM and 'audit' not in public, + 'Pending glibc proof must not claim a completed source audit') + require(not ready, 'glibc nscd complete source audit is pending; publication is blocked') + return public + GUARD.validate_glibc_nscd_source(public, item) + native = json.loads((HERE.parent / 'glibc/glibc.lock.json').read_text(encoding='utf-8')) + require(public['sourceIdentity'] == {key: native[key] for key in ('ubuntuSourceVersion', 'sourceFiles', 'patches')}, + 'glibc nscd audit differs from the exact Ubuntu/custom patch source inputs') + return public + + +def glibc_nscd_qualification(item, packages, payloads): + public = load_glibc_nscd_source_audit(item, ready=True) + selected = {p['name']: p for p in packages if p['family'] == 'glibc'} + audited = {p['name']: p for p in public['audit']['packages']} + require(set(selected) == set(FAMILIES['glibc']), 'Wrong glibc absence payload catalog') + for name, payload in payloads.items(): + require(name in selected and payload['package'] == name + and payload['version'] == GUARD.GLIBC_NSCD_VERSION + and payload['source'] == 'glibc' and payload['architecture'] == 'amd64' + and sha(GUARD.canonical_json(payload)) == selected[name]['payload']['sha256'] + and selected[name]['payload']['sha256'] == audited[name]['qualifierPayload']['sha256'] + and not any(PurePosixPath(row['path']).name == 'nscd' for row in payload['files']), + 'A selected glibc payload contains or cannot exclude the standalone nscd daemon') + require(set(payloads) == set(selected), 'Actual glibc payload evidence is incomplete') + return {'sourceClosure': public, 'sourceClosureSha256': file_sha(GLIBC_AUDIT_FILE), + 'packages': {name: {'purl': purl(p), 'archiveSha256': p['archive']['sha256'], + 'payloadSha256': p['payload']['sha256'], 'nscdDaemonPaths': []} + for name, p in sorted(selected.items())}} + + def load_rust_source_audit(item, provenance=None): ref = item.get('sourceAudit', {}) require(ref.get('path') == RUST_AUDIT_FILE.name @@ -671,7 +718,7 @@ def qualify(lock): 'sourceFiles': {file.relative_to(ROOT).as_posix(): file_sha(file) for file in sorted((HERE / 'installer').rglob('*')) if file.is_file()}} guard_proof = export_metadata_guard(output) - families, packages, qualifications = [], [], [] + families, packages, qualifications, glibc_payloads = [], [], [], {} for family in FAMILIES: if family == 'pcre2': item = lock['pcre2'] @@ -706,11 +753,16 @@ def qualify(lock): archive = output / 'debs' / package['file']; archive.parent.mkdir(exist_ok=True) archive.write_bytes(files[package['file']]) payload = package_payload(archive, package, WORK / 'elf-probes') + if family == 'glibc': glibc_payloads[package['name']] = payload payload_path = output / 'payload' / (package['name'] + '.json'); write_json(payload_path, payload) packages.append({'name': package['name'], 'family': family, 'version': package['version'], 'architecture': 'amd64', 'source': family, 'archive': {'path': 'debs/' + package['file'], 'sha256': package['sha256']}, 'payload': {'path': 'payload/' + package['name'] + '.json', 'sha256': file_sha(payload_path)}}) + glibc = next(q for q in qualifications if q['name'] == 'glibc') + glibc['codeAbsence'] = glibc_nscd_qualification(lock['nativeInputs']['glibc'], packages, glibc_payloads) + absence_purls = GUARD.validate_glibc_nscd_qualification({q['name']: q for q in qualifications}, + {p['name']: p for p in packages}, lock) write_json(output / 'qualification.json', {'schemaVersion': 1, 'bundleVersion': '0.1.0', 'platform': PLATFORM, 'status': 'qualified', 'families': qualifications, 'sourceLockSha256': file_sha(LOCK_FILE), 'installer': installer_proof, @@ -738,6 +790,7 @@ def qualify(lock): else 'https://www.cve.org/CVERecord?id=') + vulnerability, 'name': vulnerability}, 'products': [{'@id': purl(p)} for p in packages if p['family'] == family], 'status': 'fixed', 'action_statement': item['vexScope']}) + statements.append(GUARD.glibc_nscd_statement(absence_purls)) write_json(output / 'openvex.json', {'@context': 'https://openvex.dev/ns/v0.2.0', '@id': 'https://github.com/PastureStack/server/ubuntu-security-backports/0.1.0', 'author': 'PastureStack', 'timestamp': lock['vexTimestamp'], 'version': 1, 'statements': statements}) @@ -749,6 +802,8 @@ def record_consumer_tests(): output = WORK / 'context' / 'out' qualification = json.loads((output / 'qualification.json').read_text()) require('consumerFixtures' not in qualification, 'Consumer qualification cannot be overwritten') + semantics = json.loads((WORK / 'glibc-vex-semantic/result.json').read_text()) + GUARD.validate_glibc_vex_semantics(semantics, file_sha(output / 'openvex.json'), file_sha(output / 'source-inputs.lock.json')) manifest_sha = file_sha(output / 'manifest.json') records = [] checks = ['batch-install', 'verify-installed', 'package-set', 'provider', 'docs-restored', @@ -773,6 +828,7 @@ def record_consumer_tests(): records.append({'provider': provider, 'status': 'PASS', 'inputManifestSha256': manifest_sha, 'selectedPackages': selected, 'checks': checks, 'evidence': evidence}) qualification['consumerFixtures'] = records + qualification['glibcNscdVexSemanticTest'] = semantics qualification['consumerStatus'] = ('Both isolated provider fixtures passed actual installation and verification; ' 'real consumer image rootfs scanning and product acceptance remain required.') write_json(output / 'qualification.json', qualification) diff --git a/server/security/ubuntu-backports/bundle/source-inputs.lock.json b/server/security/ubuntu-backports/bundle/source-inputs.lock.json index 3502e3135d..afd970aab0 100644 --- a/server/security/ubuntu-backports/bundle/source-inputs.lock.json +++ b/server/security/ubuntu-backports/bundle/source-inputs.lock.json @@ -62,6 +62,10 @@ "reviewedDifference": "Native source and effective CI recipe are identical to the approved current files." } }, + "codeAbsenceProof": { + "path": "glibc-nscd-source-closure.json", + "sha256": "dcc574c006f401e1169e5a73c01ed32598f13fa917c448e215fc0fb76aad2657" + }, "copyright": { "file": "COPYRIGHT", "sha256": "c31d10900fdb95d1889005145a2205bb75e3bc018d3ec8265b81364901748e09" diff --git a/server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py b/server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py new file mode 100644 index 0000000000..71300b39fc --- /dev/null +++ b/server/security/ubuntu-backports/bundle/test-trivy-glibc-vex.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Assert native Trivy OpenVEX semantics in an isolated publisher fixture. + +This helper never filters findings or invokes a scanner. The workflow runs +the pinned native Trivy image and retains both reports without rewriting them. +No host, installed-package or production-image inspection is performed here. +""" +import argparse +from collections import Counter +import hashlib +import importlib.util +import json +from pathlib import Path +import sys +import tempfile + +HERE = Path(__file__).resolve().parent +GUARD_SPEC = importlib.util.spec_from_file_location('semantic_metadata_guard', HERE / 'verify-consumer-evidence.py') +GUARD = importlib.util.module_from_spec(GUARD_SPEC) +GUARD_SPEC.loader.exec_module(GUARD) +WORK = Path(tempfile.gettempdir()) / 'pasturestack-ubuntu-security-backports-v0.1.0' +FIXTURE = WORK / 'glibc-vex-semantic' +CVE = GUARD.GLIBC_NSCD_CVE +VERSION = GUARD.GLIBC_NSCD_VERSION +NAMES = tuple(sorted(GUARD.FAMILIES['glibc'])) + + +def require(ok, message): + if not ok: + raise ValueError(message) + + +def sha(data): + return hashlib.sha256(data).hexdigest() + + +def encoded(value): + return (json.dumps(value, indent=2, sort_keys=True) + '\n').encode() + + +def purl(name, version=VERSION): + return GUARD.package_purl({'name': name, 'version': version}) + + +def fixture_document(): + rows = [(name, VERSION) for name in NAMES] + [('nscd', VERSION), ('libc6', '2.43-2ubuntu2.4')] + packages = [{'type': 'library', 'name': name, 'version': version, + 'purl': purl(name, version), 'bom-ref': purl(name, version), + 'properties': [{'name': 'aquasecurity:trivy:SrcName', 'value': 'glibc'}, + {'name': 'aquasecurity:trivy:SrcVersion', 'value': version}, + {'name': 'aquasecurity:trivy:PkgID', 'value': name + '@' + version}]} + for name, version in rows] + return {'bomFormat': 'CycloneDX', 'specVersion': '1.6', 'version': 1, + 'metadata': {'component': {'type': 'file', 'name': 'glibc-vex-semantic-fixture', 'bom-ref': 'fixture'}}, + 'components': [{'type': 'operating-system', 'name': 'ubuntu', 'version': '26.04', + 'bom-ref': 'ubuntu-26.04'}] + packages, + 'dependencies': [{'ref': 'fixture', 'dependsOn': ['ubuntu-26.04']}, + {'ref': 'ubuntu-26.04', 'dependsOn': [p['bom-ref'] for p in packages]}]} + + +def findings(report): + rows = Counter() + for result in report.get('Results', []): + for row in result.get('Vulnerabilities') or []: + identifier = row.get('PkgIdentifier', {}).get('PURL') + require(isinstance(identifier, str) and identifier.startswith('pkg:deb/ubuntu/'), + 'Native Trivy finding lacks its exact Ubuntu package PURL') + rows[(row['VulnerabilityID'], identifier)] += 1 + return rows + + +def assert_raw(report): + rows = findings(report) + expected = {purl(name) for name in NAMES} | {purl('nscd'), purl('libc6', '2.43-2ubuntu2.4')} + require({p for (c, p) in rows if c == CVE} == expected + and all(rows[(CVE, p)] == 1 for p in expected), + 'Native multi-version Ubuntu SBOM/DB did not retain all five exact 89092 controls') + require(any(c != CVE for c, _ in rows), 'Native raw report lacks an independent vulnerability control') + return rows + + +def assert_effective(raw, effective): + before, after = assert_raw(raw), findings(effective) + removed = Counter({(CVE, purl(name)): 1 for name in NAMES}) + require(after == before - removed, + 'Native OpenVEX must remove only the three qualified pairs; nscd, old libc and other findings must remain') + return {'rawFindings': sum(before.values()), 'effectiveFindings': sum(after.values()), + 'removedPairs': [{'cve': c, 'purl': p} for c, p in sorted(removed)], + 'retainedNegativeControlPURLs': sorted([purl('nscd'), purl('libc6', '2.43-2ubuntu2.4')])} + + +def prepare(source_only=False): + require(not FIXTURE.exists(), 'Semantic fixture output must be new') + source_lock = (HERE / 'source-inputs.lock.json').read_bytes() + lock = json.loads(source_lock); item = lock['nativeInputs']['glibc'] + proof_bytes = (HERE / GUARD.GLIBC_NSCD_PROOF_FILE).read_bytes() + require(sha(proof_bytes) == item['codeAbsenceProof']['sha256'], 'Source fixture proof bytes differ from lock') + GUARD.validate_glibc_nscd_source(json.loads(proof_bytes), item) + expected = GUARD.glibc_nscd_statement(sorted(GUARD.package_purl(p) for p in item['packages'])) + if source_only: + doc = {'@context': 'https://openvex.dev/ns/v0.2.0', + '@id': 'https://github.com/PastureStack/server/ubuntu-security-backports/semantic-fixture', + 'author': 'PastureStack native semantic fixture', 'timestamp': lock['vexTimestamp'], + 'version': 1, 'statements': [expected]} + candidate = encoded(doc) + else: + candidate = (WORK / 'context/out/openvex.json').read_bytes() + doc = json.loads(candidate) + rows = [s for s in doc['statements'] if s['vulnerability']['name'] == CVE] + require(rows == [expected], + 'Candidate does not contain the exact qualified code-absence statement') + doc['statements'] = rows + FIXTURE.mkdir(parents=True) + (FIXTURE / 'fixture.cdx.json').write_bytes(encoded(fixture_document())) + (FIXTURE / 'scoped.openvex.json').write_bytes(encoded(doc)) + (FIXTURE / 'input.json').write_bytes(encoded({'inputOpenVexSha256': sha(candidate), + 'inputKind': 'source-lock-public-proof' if source_only else 'qualified-bundle', + 'inputSourceLockSha256': sha(source_lock), 'sourceClosureSha256': sha(proof_bytes), + 'fixtureSbomSha256': sha((FIXTURE / 'fixture.cdx.json').read_bytes()), + 'scopedOpenVexSha256': sha((FIXTURE / 'scoped.openvex.json').read_bytes())})) + + +def check_inputs(): + pins = json.loads((FIXTURE / 'input.json').read_bytes()) + for name, key in [('fixture.cdx.json', 'fixtureSbomSha256'), ('scoped.openvex.json', 'scopedOpenVexSha256')]: + require(sha((FIXTURE / name).read_bytes()) == pins[key], 'Semantic fixture input changed: ' + name) + require(sha((HERE / 'source-inputs.lock.json').read_bytes()) == pins['inputSourceLockSha256'] + and sha((HERE / GUARD.GLIBC_NSCD_PROOF_FILE).read_bytes()) == pins['sourceClosureSha256'], + 'Source lock/public proof changed during native semantic test') + candidate = WORK / 'context/out/openvex.json' if pins['inputKind'] == 'qualified-bundle' else FIXTURE / 'scoped.openvex.json' + require(sha(candidate.read_bytes()) == pins['inputOpenVexSha256'], 'Producer/fixture VEX changed during native semantic test') + require((FIXTURE / 'trivy-version.txt').read_text().splitlines()[0] == 'Version: 0.74.0', + 'Semantic test must use native Trivy 0.74.0') + return pins + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_mutually_exclusive_group(required=True) + for name in ('prepare', 'prepare-source', 'record-raw', 'verify'): modes.add_argument('--' + name, action='store_true') + args = parser.parse_args() + if args.prepare or args.prepare_source: prepare(source_only=args.prepare_source); return + pins = check_inputs(); raw_bytes = (FIXTURE / 'raw.json').read_bytes(); raw = json.loads(raw_bytes) + assert_raw(raw) + if args.record_raw: + require(not (FIXTURE / 'raw-pin.json').exists(), 'Native raw pin must be new') + (FIXTURE / 'raw-pin.json').write_bytes(encoded({'rawSha256': sha(raw_bytes)})); return + require(sha(raw_bytes) == json.loads((FIXTURE / 'raw-pin.json').read_bytes())['rawSha256'], + 'Native raw report was modified after its first retained read') + effective_bytes = (FIXTURE / 'effective.json').read_bytes() + result = dict(assert_effective(raw, json.loads(effective_bytes)), schemaVersion=1, status='PASS', + vulnerability=CVE, nativeTrivyVersion='0.74.0', **pins, rawSha256=sha(raw_bytes), + effectiveSha256=sha(effective_bytes), rawRetainedUnchanged=True, + findingsFilteredByThisHelper=False) + require(not (FIXTURE / 'result.json').exists(), 'Native semantic receipt must be new') + (FIXTURE / 'result.json').write_bytes(encoded(result)) + print('GLIBC_NSCD_NATIVE_VEX_SEMANTICS_PASS') + + +if __name__ == '__main__': + try: + main() + except (OSError, ValueError, KeyError, TypeError, UnicodeError) as error: + print('GLIBC_NSCD_VEX_SEMANTICS_FAILED: ' + str(error), file=sys.stderr) + sys.exit(1) diff --git a/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py b/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py index efce04e9b1..7950142cc7 100644 --- a/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py +++ b/server/security/ubuntu-backports/bundle/test_qualify_native_packages.py @@ -287,6 +287,67 @@ def test_rust_original18_require_bound_per_cve_source_audit(self): self.assertEqual(len(tracked['vulnerabilities']), 18) self.assertEqual(tracked['status'], 'qualified_component_source_closed') + def test_glibc_requires_complete_detached_audit_and_exact_native_debs(self): + item = self.lock['nativeInputs']['glibc'] + proof = Q.load_glibc_nscd_source_audit(item, ready=True) + self.assertEqual(proof['status'], 'validated') + self.assertFalse(proof['scope']['wholeImageUnaffectedClaimed']) + self.assertTrue(proof['scope']['libcNscdIpcClientsMayBePresent']) + self.assertEqual(proof['scope']['standaloneDaemonPackage'], 'nscd') + self.assertNotIn('productInventoryCrosscheck', proof['audit']) + self.assertNotIn('qualifiedFamilyCrosscheck', proof['audit']) + self.assertNotIn(Q.GUARD.GLIBC_NSCD_CVE, item['fixedVulnerabilities']) + self.assertEqual(Q.load_lock(ready=True)['nativeInputs']['glibc']['codeAbsenceProof']['sha256'], + Q.GUARD.GLIBC_NSCD_PROOF_SHA256) + + def test_glibc_pending_progress_cannot_authorize_publication(self): + pending = {'schemaVersion': 1, 'status': 'pending-source-closure', + 'cve': Q.GUARD.GLIBC_NSCD_CVE, 'platform': Q.PLATFORM} + file = self.root / Q.GLIBC_AUDIT_FILE.name; Q.write_json(file, pending) + item = copy.deepcopy(self.lock['nativeInputs']['glibc']) + item['codeAbsenceProof']['sha256'] = Q.file_sha(file) + with patch.object(Q, 'GLIBC_AUDIT_FILE', file), patch.object(Q.GUARD, 'GLIBC_NSCD_PROOF_SHA256', None): + self.assertEqual(Q.load_glibc_nscd_source_audit(item)['status'], 'pending-source-closure') + with self.assertRaisesRegex(ValueError, 'pending'): + Q.load_glibc_nscd_source_audit(item, ready=True) + + def test_glibc_self_rehashed_or_rebound_source_proof_is_rejected(self): + original = json.loads(Q.GLIBC_AUDIT_FILE.read_text(encoding='utf-8')) + for number, mutate in enumerate([ + lambda p: p.update(status='pending-source-closure'), + lambda p: p['scope'].update(wholeImageUnaffectedClaimed=True), + lambda p: p['nativeIdentity'].update(zipSha256='0' * 64), + lambda p: p['debs']['libc6'].update(sha256='0' * 64), + lambda p: p['audit']['effectiveSourceClosure'].update(status='pending')]): + public = copy.deepcopy(original); mutate(public) + file = self.root / Q.GLIBC_AUDIT_FILE.name; Q.write_json(file, public) + item = copy.deepcopy(self.lock['nativeInputs']['glibc']); item['codeAbsenceProof']['sha256'] = Q.file_sha(file) + with self.subTest(number=number), patch.object(Q, 'GLIBC_AUDIT_FILE', file), self.assertRaises(ValueError): + Q.load_glibc_nscd_source_audit(item, ready=True) + item = copy.deepcopy(self.lock['nativeInputs']['glibc']); item['fixedVulnerabilities'].append(Q.GUARD.GLIBC_NSCD_CVE) + with self.assertRaises(ValueError): Q.load_glibc_nscd_source_audit(item, ready=True) + + def test_glibc_actual_payload_binding_does_not_accept_a_daemon(self): + item = self.lock['nativeInputs']['glibc'] + for daemon in [False, True]: + payloads = {p['name']: {'schemaVersion': 1, 'package': p['name'], 'version': p['version'], + 'source': 'glibc', 'architecture': 'amd64', 'elf': [], + 'files': [{'path': 'usr/sbin/nscd' if daemon and p['name'] == 'libc6' else 'usr/lib/' + p['name']}]} + for p in item['packages']} + packages = [{'name': p['name'], 'family': 'glibc', 'version': p['version'], + 'archive': {'sha256': p['sha256']}, 'payload': {'sha256': Q.sha(Q.GUARD.canonical_json(payloads[p['name']]))}} + for p in item['packages']] + # Mock only the already-reviewed upstream audit dependency. The + # production loader still requires its hard-pinned real proof. + public = {'audit': {'packages': [{'name': p['name'], 'qualifierPayload': {'sha256': p['payload']['sha256']}} + for p in packages]}} + with patch.object(Q, 'load_glibc_nscd_source_audit', return_value=public): + if daemon: + with self.assertRaises(ValueError): Q.glibc_nscd_qualification(item, packages, payloads) + else: + result = Q.glibc_nscd_qualification(item, packages, payloads) + self.assertEqual(set(result['packages']), set(Q.FAMILIES['glibc'])) + def test_exported_guard_is_exact_source_bytes_and_sha(self): proof = Q.export_metadata_guard(self.root) original = (HERE / 'verify-consumer-evidence.py').read_bytes() @@ -327,6 +388,22 @@ def consumer_fixture(self, work, wrong=None): output = work / 'context/out'; output.mkdir(parents=True) Q.write_json(output / 'qualification.json', {'status': 'qualified'}) Q.write_json(output / 'manifest.json', {'qualification': {'sha256': Q.file_sha(output / 'qualification.json')}}) + Q.write_json(output / 'openvex.json', {'statements': []}) + (output / 'source-inputs.lock.json').write_bytes(Q.LOCK_FILE.read_bytes()) + Q.write_json(work / 'glibc-vex-semantic/result.json', { + 'schemaVersion': 1, 'status': 'PASS', 'vulnerability': Q.GUARD.GLIBC_NSCD_CVE, + 'nativeTrivyVersion': '0.74.0', 'inputOpenVexSha256': Q.file_sha(output / 'openvex.json'), + 'inputKind': 'qualified-bundle', 'inputSourceLockSha256': Q.file_sha(output / 'source-inputs.lock.json'), + 'sourceClosureSha256': Q.GUARD.GLIBC_NSCD_PROOF_SHA256, + 'rawRetainedUnchanged': True, 'findingsFilteredByThisHelper': False, + 'removedPairs': [{'cve': Q.GUARD.GLIBC_NSCD_CVE, + 'purl': Q.purl({'name': n, 'version': Q.GUARD.GLIBC_NSCD_VERSION})} + for n in sorted(Q.FAMILIES['glibc'])], + 'retainedNegativeControlPURLs': sorted([ + Q.purl({'name': 'nscd', 'version': Q.GUARD.GLIBC_NSCD_VERSION}), + Q.purl({'name': 'libc6', 'version': '2.43-2ubuntu2.4'})]), + 'rawFindings': 6, 'effectiveFindings': 3, + **{k: 'a' * 64 for k in ('rawSha256', 'effectiveSha256', 'fixtureSbomSha256', 'scopedOpenVexSha256')}}) digest = Q.file_sha(output / 'manifest.json') for provider in ['gnu', 'uutils']: selected = sorted(n for names in Q.FAMILIES.values() for n in names @@ -349,6 +426,7 @@ def test_two_consumer_receipts_are_bound_to_actual_input_manifest(self): qualification = json.loads((output / 'qualification.json').read_text()) manifest = json.loads((output / 'manifest.json').read_text()) self.assertEqual(len(qualification['consumerFixtures']), 2) + self.assertEqual(qualification['glibcNscdVexSemanticTest']['status'], 'PASS') self.assertEqual(manifest['qualification']['sha256'], Q.file_sha(output / 'qualification.json')) self.assertTrue((output / 'provenance/consumer/gnu/install.json').is_file()) @@ -360,6 +438,18 @@ def test_consumer_wrong_manifest_new_rust_or_skipped_check_is_rejected(self): with patch.object(Q, 'WORK', work), self.assertRaisesRegex(ValueError, 'receipt failed'): Q.record_consumer_tests() + def test_native_vex_semantic_receipt_is_required_before_final_qualification(self): + for number, mutation in enumerate([lambda r: r.update(inputOpenVexSha256='0' * 64), + lambda r: r.update(rawRetainedUnchanged=False), lambda r: r['removedPairs'].pop(), + lambda r: r['retainedNegativeControlPURLs'].pop(), lambda r: r.update(status='pending'), + lambda r: r.update(inputKind='source-lock-public-proof'), + lambda r: r.update(inputSourceLockSha256='0' * 64)]): + work = self.root / ('semantic-' + str(number)); self.consumer_fixture(work) + p = work / 'glibc-vex-semantic/result.json'; result = json.loads(p.read_text()); mutation(result) + Q.write_json(p, result) + with self.subTest(number=number), patch.object(Q, 'WORK', work), self.assertRaises(ValueError): + Q.record_consumer_tests() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py b/server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py index 55ea2d7388..7b6052e551 100644 --- a/server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py +++ b/server/security/ubuntu-backports/bundle/test_verify_consumer_evidence.py @@ -4,10 +4,16 @@ import json from pathlib import Path import unittest +import tempfile +from unittest.mock import patch SPEC = importlib.util.spec_from_file_location('consumer_evidence', Path(__file__).with_name('verify-consumer-evidence.py')) guard = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(guard) +TRIVY_SPEC = importlib.util.spec_from_file_location('trivy_semantic_fixture', + Path(__file__).with_name('test-trivy-glibc-vex.py')) +trivy_fixture = importlib.util.module_from_spec(TRIVY_SPEC) +TRIVY_SPEC.loader.exec_module(trivy_fixture) DIGEST = 'sha256:' + 'a' * 64 @@ -15,7 +21,21 @@ def encoded(value): return (json.dumps(value, sort_keys=True) + '\n').encode() -def fixture(): +def semantic_receipt(vex_sha, source_lock_sha): + return {'schemaVersion': 1, 'status': 'PASS', 'vulnerability': guard.GLIBC_NSCD_CVE, + 'nativeTrivyVersion': '0.74.0', 'inputOpenVexSha256': vex_sha, + 'inputKind': 'qualified-bundle', 'inputSourceLockSha256': source_lock_sha, + 'sourceClosureSha256': guard.GLIBC_NSCD_PROOF_SHA256, + 'rawRetainedUnchanged': True, 'findingsFilteredByThisHelper': False, + 'removedPairs': [{'cve': guard.GLIBC_NSCD_CVE, 'purl': trivy_fixture.purl(n)} + for n in sorted(guard.FAMILIES['glibc'])], + 'retainedNegativeControlPURLs': sorted([trivy_fixture.purl('nscd'), + trivy_fixture.purl('libc6', '2.43-2ubuntu2.4')]), + 'rawFindings': 6, 'effectiveFindings': 3, + **{k: 'a' * 64 for k in ('rawSha256', 'effectiveSha256', 'fixtureSbomSha256', 'scopedOpenVexSha256')}} + + +def fixture(absence=False): source = {'schemaVersion': 1, 'bundleVersion': '0.1.0', 'platform': guard.PLATFORM} manifest = dict(source, families=[], packages=[]) qualification = dict(source, status='qualified', sourceLockSha256=guard.sha(encoded(source)), families=[]) @@ -37,6 +57,33 @@ def fixture(): vex = {'@context': 'https://openvex.dev/ns/v0.2.0', 'statements': [ {'vulnerability': {'name': 'CVE-2026-35370'}, 'status': 'fixed', 'action_statement': 'Exact official patch qualified.', 'products': [{'@id': guard.package_purl(p)} for p in manifest['packages']]}]} + if absence: + here = Path(__file__).parent + native = json.loads((here / 'source-inputs.lock.json').read_text())['nativeInputs']['glibc'] + public = json.loads((here / guard.GLIBC_NSCD_PROOF_FILE).read_text()) + source['nativeInputs'] = {'glibc': copy.deepcopy(native)} + family = next(f for f in manifest['families'] if f['name'] == 'glibc') + family.update(version=guard.GLIBC_NSCD_VERSION) + family['provenance']['sha256'] = native['evidence']['provenance.json'] + q = next(f for f in qualification['families'] if f['name'] == 'glibc') + q.update(nativeInput=copy.deepcopy(native), provenanceSha256=native['evidence']['provenance.json']) + audited = {p['name']: p for p in public['audit']['packages']} + q['codeAbsence'] = {'sourceClosure': public, 'sourceClosureSha256': guard.GLIBC_NSCD_PROOF_SHA256, 'packages': {}} + for p in manifest['packages']: + if p['family'] != 'glibc': continue + p['version'] = guard.GLIBC_NSCD_VERSION + p['archive']['sha256'] = public['debs'][p['name']]['sha256'] + p['payload']['sha256'] = audited[p['name']]['qualifierPayload']['sha256'] + q['codeAbsence']['packages'][p['name']] = {'purl': guard.package_purl(p), + 'archiveSha256': p['archive']['sha256'], 'payloadSha256': p['payload']['sha256'], 'nscdDaemonPaths': []} + vex['statements'][0]['products'] = [{'@id': guard.package_purl(p)} for p in manifest['packages'] if p['family'] != 'glibc'] + vex['statements'].append({'vulnerability': {'name': 'CVE-2026-18374'}, 'status': 'fixed', + 'action_statement': 'Exact native official fix.', + 'products': [{'@id': trivy_fixture.purl(n)} for n in sorted(guard.FAMILIES['glibc'])]}) + vex['statements'].append(guard.glibc_nscd_statement(sorted(trivy_fixture.purl(n) for n in guard.FAMILIES['glibc']))) + qualification['sourceLockSha256'] = guard.sha(encoded(source)) + qualification['glibcNscdVexSemanticTest'] = semantic_receipt(guard.sha(encoded(vex)), guard.sha(encoded(source))) + manifest['qualification']['sha256'] = guard.sha(encoded(qualification)) files = {name: encoded(value) for name, value in [('manifest.json', manifest), ('qualification.json', qualification), ('source-inputs.lock.json', source), ('install.json', install), ('verify.json', verified), ('openvex.json', vex)]} files['bundle-image.txt'] = ('ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0@' + DIGEST + '\n').encode() @@ -57,7 +104,105 @@ def change(files, name, mutation): doc = json.loads(files[name]); mutation(doc); files[name] = encoded(doc) +def refresh_bindings(files): + change(files, 'qualification.json', lambda q: q.update(sourceLockSha256=guard.sha(files['source-inputs.lock.json']))) + change(files, 'manifest.json', lambda m: m['qualification'].update(sha256=guard.sha(files['qualification.json']))) + + class ConsumerEvidenceTests(unittest.TestCase): + def test_source_native_fixture_requires_no_debs_and_uses_production_generator(self): + with tempfile.TemporaryDirectory(prefix='glibc-vex-source-unit-') as directory: + work = Path(directory); target = work / 'fixture' + with patch.object(trivy_fixture, 'FIXTURE', target), patch.object(trivy_fixture, 'WORK', work): + trivy_fixture.prepare(source_only=True) + doc = json.loads((target / 'scoped.openvex.json').read_bytes()) + pins = json.loads((target / 'input.json').read_bytes()) + self.assertEqual(doc['statements'], [guard.glibc_nscd_statement(sorted( + trivy_fixture.purl(n) for n in guard.FAMILIES['glibc']))]) + self.assertEqual(pins['inputKind'], 'source-lock-public-proof') + self.assertEqual({p.name for p in target.iterdir()}, {'fixture.cdx.json', 'scoped.openvex.json', 'input.json'}) + + def test_exact_source_bound_glibc_absence_is_distinct_from_fixed(self): + files, sbom = fixture(absence=True); original = copy.deepcopy((files, sbom)) + result = guard.verify(files, checksums(files), sbom, DIGEST, producer_checksums(files)) + self.assertEqual(result['qualifiedCodeAbsentVexPairs'], 3) + self.assertEqual(result['qualifiedFixedVexPairs'], 12) + self.assertEqual((files, sbom), original) + self.assertEqual(len(guard.FILES), 7) + + def test_glibc_absence_cannot_expand_cve_package_version_arch_distro_or_status(self): + for kind in ['cve', 'alias', 'fixed', 'justification', 'impact', 'nscd', 'dev', 'old-version', + 'arch', 'distro', 'missing-product', 'extra-product', 'missing-statement', 'duplicate']: + with self.subTest(kind=kind): + files, sbom = fixture(absence=True); doc = json.loads(files['openvex.json']); row = doc['statements'][-1] + if kind == 'cve': row['vulnerability']['name'] = 'CVE-2026-89093' + if kind == 'alias': row['vulnerability']['aliases'] = ['CVE-2026-89093'] + if kind == 'fixed': row['status'] = 'fixed' + if kind == 'justification': row['justification'] = 'vulnerable_code_not_in_execute_path' + if kind == 'impact': row['impact_statement'] = 'Whole image is safe.' + if kind == 'nscd': row['products'][0]['@id'] = trivy_fixture.purl('nscd') + if kind == 'dev': row['products'][0]['@id'] = trivy_fixture.purl('libc6-dev') + if kind == 'old-version': row['products'][0]['@id'] = trivy_fixture.purl('libc6', '2.43-2ubuntu2.4') + if kind == 'arch': row['products'][0]['@id'] = row['products'][0]['@id'].replace('amd64', 'arm64') + if kind == 'distro': row['products'][0]['@id'] = row['products'][0]['@id'].replace('26.04', '24.04') + if kind == 'missing-product': row['products'].pop() + if kind == 'extra-product': row['products'].append({'@id': trivy_fixture.purl('nscd')}) + if kind == 'missing-statement': doc['statements'].pop() + if kind == 'duplicate': doc['statements'].append(copy.deepcopy(row)) + files['openvex.json'] = encoded(doc) + with self.assertRaises(ValueError): guard.verify(files, checksums(files), sbom, DIGEST, producer_checksums(files)) + + def test_absent_changed_or_self_rehashed_glibc_source_proof_is_rejected(self): + for kind in ['absent', 'pending', 'source', 'native', 'deb', 'payload', 'fixed-id', 'semantic', 'source-only-semantic']: + with self.subTest(kind=kind): + files, sbom = fixture(absence=True); q = json.loads(files['qualification.json']) + glibc = next(f for f in q['families'] if f['name'] == 'glibc') + if kind == 'absent': glibc.pop('codeAbsence') + if kind in ('pending', 'source', 'native', 'deb'): + proof = glibc['codeAbsence']['sourceClosure'] + if kind == 'pending': proof['status'] = 'pending-source-closure' + if kind == 'source': proof['sourceIdentity']['sourceFiles'][0]['sha256'] = '0' * 64 + if kind == 'native': proof['nativeIdentity']['zipSha256'] = '0' * 64 + if kind == 'deb': proof['debs']['libc6']['sha256'] = '0' * 64 + digest = guard.sha(guard.canonical_json(proof)) + glibc['codeAbsence']['sourceClosureSha256'] = digest + glibc['nativeInput']['codeAbsenceProof']['sha256'] = digest + if kind == 'payload': glibc['codeAbsence']['packages']['libc6']['payloadSha256'] = '0' * 64 + if kind == 'fixed-id': glibc['nativeInput']['fixedVulnerabilities'].append(guard.GLIBC_NSCD_CVE) + if kind == 'semantic': q.pop('glibcNscdVexSemanticTest') + if kind == 'source-only-semantic': q['glibcNscdVexSemanticTest']['inputKind'] = 'source-lock-public-proof' + files['qualification.json'] = encoded(q) + change(files, 'source-inputs.lock.json', lambda l: l['nativeInputs'].update(glibc=copy.deepcopy(glibc['nativeInput']))) + refresh_bindings(files) + with self.assertRaises(ValueError): guard.verify(files, checksums(files), sbom, DIGEST, producer_checksums(files)) + + def test_native_fixture_keeps_distinct_multi_version_ubuntu_components(self): + doc = trivy_fixture.fixture_document() + rows = [c for c in doc['components'] if c['type'] == 'library'] + self.assertEqual(len(rows), 5) + self.assertEqual(len({c['bom-ref'] for c in rows}), 5) + self.assertEqual({c['purl'] for c in rows}, set(doc['dependencies'][1]['dependsOn'])) + self.assertEqual(len([c for c in rows if c['name'] == 'libc6']), 2) + self.assertTrue(all(c['bom-ref'] == c['purl'] for c in rows)) + + def test_native_semantic_oracle_rejects_loss_of_any_negative_control(self): + pairs = [(trivy_fixture.CVE, trivy_fixture.purl(n)) for n in trivy_fixture.NAMES] + controls = [(trivy_fixture.CVE, trivy_fixture.purl('nscd')), + (trivy_fixture.CVE, trivy_fixture.purl('libc6', '2.43-2ubuntu2.4')), + ('CVE-2026-18374', trivy_fixture.purl('libc6'))] + def report(rows): + return {'Results': [{'Vulnerabilities': [{'VulnerabilityID': c, 'PkgIdentifier': {'PURL': p}} + for c, p in rows]}]} + raw = report(pairs + controls); original = copy.deepcopy(raw) + result = trivy_fixture.assert_effective(raw, report(controls)) + self.assertEqual((result['rawFindings'], result['effectiveFindings']), (6, 3)) + self.assertEqual(raw, original) + for missing in controls: + with self.subTest(missing=missing), self.assertRaises(ValueError): + trivy_fixture.assert_effective(raw, report([p for p in controls if p != missing])) + with self.assertRaises(ValueError): trivy_fixture.assert_raw(report(pairs + controls[1:])) + with self.assertRaises(ValueError): trivy_fixture.assert_effective(raw, raw) + def test_debian_epoch_matches_native_trivy_purl(self): self.assertEqual(guard.package_purl({'name': 'libxrender1', 'version': '1:0.9.12-1build1+pasturestack.1'}), diff --git a/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py b/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py index 8557d25bfc..48a316f5bc 100644 --- a/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py +++ b/server/security/ubuntu-backports/bundle/verify-consumer-evidence.py @@ -26,6 +26,16 @@ SHA256 = re.compile(r'[0-9a-f]{64}') DIGEST = re.compile(r'sha256:[0-9a-f]{64}') VULNERABILITY = re.compile(r'(?:CVE-[0-9]{4}-[0-9]+|GHSA-[23456789cfghjmpqrvwx]{4}(?:-[23456789cfghjmpqrvwx]{4}){2})') +GLIBC_NSCD_CVE = 'CVE-2026-89092' +GLIBC_NSCD_VERSION = '2.43-2ubuntu2.4+pasturestack.1' +GLIBC_NSCD_PROOF_FILE = 'glibc-nscd-source-closure.json' +# Set only after review of the complete public source/object/package audit. +# Pending progress evidence cannot authorize a not_affected statement. +GLIBC_NSCD_PROOF_SHA256 = 'dcc574c006f401e1169e5a73c01ed32598f13fa917c448e215fc0fb76aad2657' +GLIBC_NSCD_SOURCES = ['nscd/aicache.c', 'nscd/hstcache.c'] +GLIBC_NSCD_IMPACT = ('The two vulnerable nscd daemon modules are absent from these exact runtime packages. ' + 'libc nscd IPC client helpers may be present. This statement does not cover the separate nscd daemon/package ' + 'or claim that a whole consumer image is not affected.') def require(ok, message): @@ -55,6 +65,109 @@ def unique_names(values, label): return set(values) +def canonical_json(value): + return (json.dumps(value, indent=2, sort_keys=True) + '\n').encode() + + +def glibc_nscd_statement(exact_purls): + expected = sorted(package_purl({'name': n, 'version': GLIBC_NSCD_VERSION}) for n in FAMILIES['glibc']) + require(exact_purls == expected, 'glibc absence statement requires exactly three approved PURLs') + return {'vulnerability': {'@id': 'https://www.cve.org/CVERecord?id=' + GLIBC_NSCD_CVE, 'name': GLIBC_NSCD_CVE}, + 'products': [{'@id': p} for p in expected], 'status': 'not_affected', + 'justification': 'vulnerable_code_not_present', 'impact_statement': GLIBC_NSCD_IMPACT} + + +def native_glibc_identity(item): + return {key: item[key] for key in ('sourceCommit', 'runId', 'jobId', 'artifactId', 'zipSha256', 'lockSha256')} | { + 'provenanceSha256': item['evidence']['provenance.json']} + + +def validate_glibc_nscd_source(proof, item): + """One reviewed proof, one CVE, three exact binary packages; no generic waiver.""" + ref = item.get('codeAbsenceProof', {}) + require(GLIBC_NSCD_PROOF_SHA256 is not None + and ref == {'path': GLIBC_NSCD_PROOF_FILE, 'sha256': GLIBC_NSCD_PROOF_SHA256} + and sha(canonical_json(proof)) == GLIBC_NSCD_PROOF_SHA256, + 'glibc nscd complete reviewed source proof is absent or changed') + require(proof['schemaVersion'] == 1 and proof['status'] == 'validated' + and proof['cve'] == GLIBC_NSCD_CVE and proof['platform'] == PLATFORM + and proof['nativeIdentity'] == native_glibc_identity(item) + and item['status'] == 'qualified' and GLIBC_NSCD_CVE not in item['fixedVulnerabilities'], + 'glibc nscd audit does not bind this exact qualified native input') + require(proof['scope'] == {'verdict': 'component-code-absence-only', + 'customVersion': GLIBC_NSCD_VERSION, 'runtimePackages': sorted(FAMILIES['glibc']), + 'vulnerableSources': GLIBC_NSCD_SOURCES, 'standaloneDaemonPackage': 'nscd', + 'libcNscdIpcClientsMayBePresent': True, 'wholeImageUnaffectedClaimed': False} + and SHA256.fullmatch(proof['originalReceiptSha256']) + and sha(canonical_json(proof['audit'])) == proof['auditProjectionSha256'] + and proof['audit']['status'] == 'qualified' and proof['audit']['cve'] == GLIBC_NSCD_CVE, + 'glibc nscd source audit scope/authority is not complete') + audit = proof['audit']; native = audit['nativeIdentity'] + require(all(native[k] == item[k] for k in ('sourceCommit', 'runId', 'jobId', 'artifactId')) + and native['artifactZipSha256'] == item['zipSha256'] + and native['sourceLockSha256'] == item['lockSha256'] + and native['provenanceSha256'] == item['evidence']['provenance.json'] + and native['sourceRecipeFiles'] == item['sourceFiles'] + and audit['sourceIdentity']['sourceFiles'] == proof['sourceIdentity']['sourceFiles'] + and audit['sourceIdentity']['sourceLockSha256'] == item['lockSha256'] + and audit['effectiveSourceClosure']['status'] == 'validated' + and audit['buildObjectClosure']['allNativeLinkCommandsMentioningTheseObjectsTargetOnlyNscd'] is True + and audit['buildObjectClosure']['libcClientRoutinesAreTheVulnerableDaemonModules'] is False + and audit['excludedNscdPackage']['name'] == 'nscd' + and audit['excludedNscdPackage']['qualifiedExport'] is False, + 'glibc source/object/package audit does not close the selected native boundary') + require(len(item['packages']) == 3 and {p['name'] for p in item['packages']} == FAMILIES['glibc'] + and all(p['version'] == GLIBC_NSCD_VERSION and p['source'] == 'glibc' for p in item['packages']) + and proof['debs'] == {p['name']: {'file': p['file'], 'sha256': p['sha256']} for p in item['packages']}, + 'glibc nscd source audit does not cover the exact three deb archives') + + +def validate_glibc_nscd_qualification(qualified, packages, lock): + family = qualified['glibc']; item = family['nativeInput'] + require(item == lock.get('nativeInputs', {}).get('glibc'), 'glibc absence proof differs from immutable source lock') + absence = family.get('codeAbsence', {}) + validate_glibc_nscd_source(absence.get('sourceClosure', {}), item) + require(absence.get('sourceClosureSha256') == GLIBC_NSCD_PROOF_SHA256, + 'glibc source-closure hash mismatch') + expected = {name: {'purl': package_purl(packages[name]), + 'archiveSha256': packages[name]['archive']['sha256'], + 'payloadSha256': packages[name]['payload']['sha256'], 'nscdDaemonPaths': []} + for name in sorted(FAMILIES['glibc'])} + audited = {p['name']: p for p in absence['sourceClosure']['audit']['packages']} + require(absence.get('packages') == expected + and all(packages[n]['version'] == GLIBC_NSCD_VERSION + and packages[n]['archive']['sha256'] == absence['sourceClosure']['debs'][n]['sha256'] + and packages[n]['payload']['sha256'] == audited[n]['qualifierPayload']['sha256'] + for n in FAMILIES['glibc']), 'glibc absence package/payload binding mismatch') + return sorted(row['purl'] for row in expected.values()) + + +def validate_glibc_vex_semantics(receipt, vex_sha, source_lock_sha): + require(isinstance(receipt, dict) and {'schemaVersion', 'status', 'vulnerability', 'nativeTrivyVersion', + 'inputOpenVexSha256', 'rawRetainedUnchanged', 'findingsFilteredByThisHelper', 'removedPairs', + 'retainedNegativeControlPURLs', 'rawFindings', 'effectiveFindings', 'rawSha256', + 'effectiveSha256', 'fixtureSbomSha256', 'scopedOpenVexSha256', 'inputKind', + 'inputSourceLockSha256', 'sourceClosureSha256'} <= receipt.keys(), + 'Native Trivy glibc/nscd semantic receipt is absent') + expected = sorted(package_purl({'name': n, 'version': GLIBC_NSCD_VERSION}) for n in FAMILIES['glibc']) + require(receipt['schemaVersion'] == 1 and receipt['status'] == 'PASS' + and receipt['vulnerability'] == GLIBC_NSCD_CVE and receipt['nativeTrivyVersion'] == '0.74.0' + and receipt['inputKind'] == 'qualified-bundle' + and receipt['inputSourceLockSha256'] == source_lock_sha + and receipt['sourceClosureSha256'] == GLIBC_NSCD_PROOF_SHA256 + and receipt['inputOpenVexSha256'] == vex_sha and receipt['rawRetainedUnchanged'] is True + and receipt['findingsFilteredByThisHelper'] is False + and receipt['removedPairs'] == [{'cve': GLIBC_NSCD_CVE, 'purl': p} for p in expected] + and receipt['retainedNegativeControlPURLs'] == sorted([ + package_purl({'name': 'nscd', 'version': GLIBC_NSCD_VERSION}), + package_purl({'name': 'libc6', 'version': '2.43-2ubuntu2.4'})]) + and type(receipt['rawFindings']) is int and type(receipt['effectiveFindings']) is int + and receipt['rawFindings'] - receipt['effectiveFindings'] == 3 and receipt['effectiveFindings'] >= 3 + and all(SHA256.fullmatch(receipt[k]) for k in + ('rawSha256', 'effectiveSha256', 'fixtureSbomSha256', 'scopedOpenVexSha256')), + 'Native Trivy exact glibc/nscd VEX semantic qualification is missing or changed') + + def verify(files, checksums, sbom, expected_digest, bundle_checksums): require(DIGEST.fullmatch(expected_digest) is not None, 'Expected bundle digest is not immutable') expected_image = 'ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0@' + expected_digest @@ -138,25 +251,39 @@ def verify(files, checksums, sbom, expected_digest, bundle_checksums): require(vex.get('@context') == 'https://openvex.dev/ns/v0.2.0' and isinstance(vex.get('statements'), list) and vex['statements'], 'Invalid OpenVEX document') pairs = set() + absence_statements = 0 for statement in vex['statements']: identifier = statement['vulnerability']['name'] - require(VULNERABILITY.fullmatch(identifier) and statement['status'] == 'fixed' - and statement.get('action_statement') and statement['products'], - 'Only explicit qualified fixed statements are accepted') + if identifier == GLIBC_NSCD_CVE: + expected = validate_glibc_nscd_qualification(qualified, packages, docs['source-inputs.lock.json']) + require(statement == glibc_nscd_statement(expected), + 'Only the exact qualified glibc component-code-absence statement is accepted') + validate_glibc_vex_semantics(qualification.get('glibcNscdVexSemanticTest', {}), sha(files['openvex.json']), + sha(files['source-inputs.lock.json'])) + absence_statements += 1 + require(absence_statements == 1, 'Duplicate glibc code-absence statement') + else: + require(VULNERABILITY.fullmatch(identifier) and statement['status'] == 'fixed' + and statement.get('action_statement') and statement['products'], + 'Only explicit qualified fixed statements are accepted') for product in statement['products']: require(set(product) == {'@id'} and product['@id'] in purls, 'OpenVEX product is not a complete qualified exact-version PURL') package = purls[product['@id']] - require(identifier in qualified[package['family']]['nativeInput']['fixedVulnerabilities'], - 'OpenVEX CVE is outside the native qualified fix set') + if identifier != GLIBC_NSCD_CVE: + require(identifier in qualified[package['family']]['nativeInput']['fixedVulnerabilities'], + 'OpenVEX CVE is outside the native qualified fix set') pair = (identifier, product['@id']) require(pair not in pairs, 'Duplicate OpenVEX vulnerability/package statement') pairs.add(pair) + require('codeAbsenceProof' not in qualified['glibc']['nativeInput'] or absence_statements == 1, + 'Qualified glibc code-absence statement is missing') return {'schemaVersion': 1, 'status': 'PASS', 'bundleDigest': expected_digest, 'manifestSha256': sha(files['manifest.json']), 'openvexSha256': sha(files['openvex.json']), 'bundleChecksumsSha256': sha(bundle_checksums), 'producerMetadataSha256': producer, 'selectedPackages': sorted(selected), 'selectedPURLs': sorted(actual.values()), - 'qualifiedVexPairs': len(pairs), 'findingsFilteredByThisTool': False} + 'qualifiedVexPairs': len(pairs), 'qualifiedCodeAbsentVexPairs': 3 * absence_statements, + 'qualifiedFixedVexPairs': len(pairs) - 3 * absence_statements, 'findingsFilteredByThisTool': False} def bounded_read(path):