diff --git a/.github/scripts/check_buildx_code_absence.py b/.github/scripts/check_buildx_code_absence.py new file mode 100644 index 0000000..8569e3a --- /dev/null +++ b/.github/scripts/check_buildx_code_absence.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +"""Check stdin evidence before native VEX; never read a caller-selected path.""" +import argparse +from collections import Counter +import hashlib +import json +import re +import sys + +TARGET = 'usr/local/lib/docker/cli-plugins/docker-buildx' +PURL = 'pkg:golang/github.com/docker/docker@v28.5.2%2Bincompatible' +CVES = {'CVE-2026-41567', 'CVE-2026-42306'} +NAMES = 'github.com/docker/docker/pkg/namesgenerator' +MAIN = 'github.com/docker/buildx/cmd/buildx' +NAME_FILE = 'module/github.com/docker/docker@v28.5.2+incompatible/pkg/namesgenerator/names-generator.go' +NAME_SHA = 'cd2f9528ad47ae7519d78e6381e0b2313fa9b5ff541814e95e06ebb187aa9b64' +PINS = { + 'schemaVersion': '1', 'upstreamVersion': 'v0.38.1', + 'downstreamVersion': 'v0.38.1-pasturestack.1', + 'sourceCommit': '0dbe87f36ed472fe36b6796c3f70b4210a731fc5', + 'sourceArchiveSha256': 'c5c810b083ed3725f7d4299ba01505b5891d164cf81c3398ea4c3b27a3b71149', + 'originalGoModSha256': 'a93029abb94e88ddaa532ddeaab4802259a1025408f1eac417a9886e140067ef', + 'updatedGoModSha256': '230c98fa66a534b3a4d2dcdb0507e871658deff3c80058b01d85d7a18ce656b9', + 'goVersion': 'go1.27.3', 'goos': 'linux', +} +IDS = {'GO-2026-5617', 'GO-2026-5746', 'GO-2026-6253', 'GO-2026-6611'} +OSV_PINS = { + 'GO-2026-5617': '5a24060e5cc8cbf6a146615ebe5a7e5beac2f8f267c905538736ed6931ae2af5', + 'GO-2026-5746': 'd11da88232883a96ac3b74f848f172ce86c3783b7818d7da4b925335621d5273', + 'GO-2026-6253': '4f337e5aad0f8e83e3db2fc67d058fe0e80017b8c68f494b7e82be87e7120872', + 'GO-2026-6611': '5e89ac4314a24eac41111b71ef4544f22836c7b1baf48747e31c5c1d8282c3de', +} + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def unique_pairs(items): + result = {} + for key, value in items: + require(key not in result, 'Duplicate metadata key') + result[key] = value + return result + + +def load(text): + return json.loads(text, object_pairs_hook=unique_pairs) + + +def objects(text): + decoder = json.JSONDecoder(object_pairs_hook=unique_pairs) + offset = 0 + while text[offset:].strip(): + offset += len(text[offset:]) - len(text[offset:].lstrip()) + item, offset = decoder.raw_decode(text, offset) + yield item + + +def sha(text): + return hashlib.sha256(text.encode('utf-8')).hexdigest() + + +def findings(report): + for result in report.get('Results', []): + for finding in result.get('Vulnerabilities', []): + yield result['Target'], finding + + +def pair(finding): + return finding['VulnerabilityID'], finding['PkgIdentifier']['PURL'] + + +def vex_pairs(document): + pairs = [] + for statement in document['statements']: + require(statement['status'] == 'not_affected' + and statement['justification'] == 'vulnerable_code_not_present', 'Unexpected Dapper VEX status') + pairs.extend((statement['vulnerability']['name'], p['@id']) for p in statement['products']) + require(len(pairs) == len(set(pairs)), 'Duplicate VEX scope') + return set(pairs) + + +def source_check(data): + binding = unique_pairs(line.split('\t') for line in data['binding'].splitlines()) + require(set(binding) == set(PINS) | {'goarch', 'binarySha256'}, 'Incomplete source binding') + require(all(binding[k] == v for k, v in PINS.items()), 'Wrong official source/toolchain') + require(binding['goarch'] in ['amd64', 'arm64'], 'Wrong Buildx architecture') + require(re.fullmatch('[0-9a-f]{64}', data['binarySha256']) is not None + and data['binarySha256'] == binding['binarySha256'], 'Actual binary SHA differs from built source receipt') + packages = data['packages'].splitlines() + require(packages and len(packages) == len(set(packages)), 'Incomplete/duplicate compiled package inventory') + require(MAIN in packages, 'Buildx main package was not compiled') + require([p for p in packages if p == 'github.com/docker/docker' or p.startswith('github.com/docker/docker/')] == [NAMES], + 'Legacy Docker contains more than namesgenerator; daemon absence is unproven') + require('github.com/moby/go-archive/compression' in packages + and 'github.com/moby/go-archive' not in packages, 'Unexpected archive extractor compilation') + source_files = {} + for line in data['files'].splitlines(): + package, path, digest = line.split('\t') + require(package in packages and path.startswith(('source/', 'module/', 'goroot/')) + and not any(p in ['', '.', '..'] for p in path.split('/')) + and '\\' not in path and re.fullmatch('[0-9a-f]{64}', digest), 'Unsafe/invalid compiled file inventory') + require((package, path) not in source_files, 'Duplicate compiled file') + source_files[package, path] = digest + require({k: v for k, v in source_files.items() if k[0] == NAMES} == {(NAMES, NAME_FILE): NAME_SHA}, + 'Namesgenerator source bytes do not match the official module') + before, after = data['modulesBefore'].splitlines(), data['modulesAfter'].splitlines() + changed = ('golang.org/x/net ', 'github.com/moby/go-archive ') + require([s for s in before if not s.startswith(changed)] == [s for s in after if not s.startswith(changed)], + 'Unrelated module version changed') + for module, old, new in [('golang.org/x/net', 'v0.59.0', 'v0.60.0'), ('github.com/moby/go-archive', 'v0.2.0', 'v0.3.0')]: + require([s for s in before if s.startswith(module + ' ')] == [module + ' ' + old] + and [s for s in after if s.startswith(module + ' ')] == [module + ' ' + new], 'Wrong fixed module version') + info = data['buildInfo'].splitlines() + require(info and info[0].endswith(': go1.27.3'), 'Actual binary has a different compiler') + deps = {} + settings = {} + for line in info[1:]: + fields = line.strip().split('\t') + if fields[0] == 'dep': + require(fields[1] not in deps, 'Duplicate binary module'); deps[fields[1]] = fields[2] + if fields[0] == 'build': + key, value = fields[1].split('=', 1) + require(key not in settings, 'Duplicate binary setting'); settings[key] = value + require(all(settings.get(k) == v for k, v in {'GOOS': 'linux', 'GOARCH': binding['goarch'], 'CGO_ENABLED': '0'}.items()), + 'Actual binary platform differs from source check') + require(all(deps.get(k) == v for k, v in {'golang.org/x/net': 'v0.60.0', 'github.com/moby/go-archive': 'v0.3.0', + 'github.com/docker/docker': 'v28.5.2+incompatible'}.items()), 'Actual binary module identity changed') + require(f"github.com/docker/buildx {PINS['downstreamVersion']} {PINS['sourceCommit']}" in data['version'], 'Wrong downstream tool identity') + require(set(data['osvInputs']) == IDS and all(sha(data['osvInputs'][k]) == v for k, v in OSV_PINS.items()), + 'Canonical four vulnerability input bytes do not match their official pins') + checks = list(objects(data['sourceCheck'])) + configs = [x['config'] for x in checks if 'config' in x] + require(len(configs) == 1 and configs[0]['scanner_name'] == 'govulncheck' + and configs[0]['scanner_version'] == 'v1.9.0' and configs[0]['scan_mode'] == 'source' + and configs[0]['scan_level'] == 'package', 'Native package source check missing or replaced by stripped-binary fallback') + sboms = [x['SBOM'] for x in checks if 'SBOM' in x] + require(len(sboms) == 1 and sboms[0]['go_version'] == 'go1.27.3' and sboms[0]['roots'] == [MAIN], + 'Native source check did not inspect this Buildx program') + scanned = unique_pairs((m['path'], m.get('version', '')) for m in sboms[0]['modules']) + require(all(scanned.get(k) == v for k, v in deps.items()), 'Source check module versions differ from the binary') + emitted = {x['osv']['id'] for x in checks if 'osv' in x} + require({'GO-2026-5617', 'GO-2026-5746'} <= emitted <= IDS, + 'Daemon source checks missing or an unbounded OSV was emitted') + for item in checks: + if 'finding' in item: + finding = item['finding'] + require(finding['osv'] in IDS, 'Unbounded source-check result') + require(not any(frame.get('package') or frame.get('function') for frame in finding['trace']), + 'Vulnerable package or function is present in the source check') + raw = load(data['raw']) + scoped = [(target, f) for target, f in findings(raw) if f.get('PkgIdentifier', {}).get('PURL') == PURL] + require(scoped and all(target == TARGET for target, _ in scoped), 'The same legacy PURL occurs in another binary') + require({f['VulnerabilityID'] for _, f in scoped if f['VulnerabilityID'] in CVES} == CVES, + 'The two raw daemon controls are missing') + require({p for p in vex_pairs(data['vex']) if p[1] == PURL} == {(cve, PURL) for cve in CVES}, + 'Daemon VEX exceeds the two qualified scopes') + return {'schemaVersion': 1, 'status': 'BUILDX_DAEMON_CODE_ABSENCE_VERIFIED', 'target': TARGET, + 'purl': PURL, 'sourceCommit': binding['sourceCommit'], 'sourceArchiveSha256': binding['sourceArchiveSha256'], + 'binarySha256': binding['binarySha256'], 'rawSha256': sha(data['raw']), + 'vexSha256': sha(json.dumps(data['vex'], sort_keys=True)), + 'compiledPackageCount': len(packages), 'compiledSourceFileCount': len(source_files), + 'packagesSha256': sha(data['packages']), 'sourceFilesSha256': sha(data['files']), + 'modulesBeforeSha256': sha(data['modulesBefore']), 'modulesAfterSha256': sha(data['modulesAfter']), + 'canonicalOsvSha256': dict(OSV_PINS), 'emittedOsv': sorted(emitted), + 'sourceCheckSha256': sha(data['sourceCheck']), 'scope': sorted(CVES)} + + +def native_check(data): + require(data['guard']['status'] == 'BUILDX_DAEMON_CODE_ABSENCE_VERIFIED' + and sha(data['raw']) == data['guard']['rawSha256'] + and sha(json.dumps(data['vex'], sort_keys=True)) == data['guard']['vexSha256'], + 'Raw report or VEX changed after the source guard') + raw, effective = load(data['raw']), load(data['effective']) + allowed = vex_pairs(data['vex']) + canonical = lambda target, finding: json.dumps([target, finding], sort_keys=True, separators=(',', ':')) + kept = Counter(canonical(t, f) for t, f in findings(raw) if pair(f) not in allowed) + actual = Counter(canonical(t, f) for t, f in findings(effective)) + require(kept == actual, 'Native VEX changed an unrelated finding or retained a qualified scope') + return {'schemaVersion': 1, 'status': 'NATIVE_DAPPER_VEX_VERIFIED', 'rawSha256': sha(data['raw']), + 'effectiveSha256': sha(data['effective']), 'retainedFindings': sum(actual.values()), + 'rawFindings': sum(1 for _ in findings(raw)), 'rawRetainedUnchanged': True} + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--native', action='store_true') + args = parser.parse_args() + try: + content = sys.stdin.buffer.read(64 * 1024 * 1024 + 1) + require(len(content) <= 64 * 1024 * 1024, 'Evidence exceeds size limit') + data = load(content.decode('utf-8')) + print(json.dumps(native_check(data) if args.native else source_check(data), sort_keys=True)) + except (ValueError, KeyError, TypeError, UnicodeError) as error: + print('BUILDX_CODE_ABSENCE_FAILED: ' + str(error), file=sys.stderr) + sys.exit(1) diff --git a/.github/scripts/collect-ubuntu-backport-evidence.sh b/.github/scripts/collect-ubuntu-backport-evidence.sh new file mode 100644 index 0000000..6896641 --- /dev/null +++ b/.github/scripts/collect-ubuntu-backport-evidence.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Only read the exact consumer and immutable bundle; execute its guard on the runner. +set -euo pipefail +test "$#" -eq 3 +image=$1 +sbom=$2 +evidence=$3 +bundle_image='ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0@sha256:2742a3d429e4e14bc587f6dedc51ded4246cf90bbaedcd86c67d0885e32a6f2a' +bundle_digest=${bundle_image##*@} +[[ "$bundle_digest" =~ ^sha256:[0-9a-f]{64}$ ]] +test -s "$sbom" +mkdir -p "$evidence" +scratch=$(mktemp -d) +consumer= +bundle= +cleanup() { + if [[ -n "$consumer" ]]; then docker rm "$consumer" >/dev/null 2>&1 || true; fi + if [[ -n "$bundle" ]]; then docker rm "$bundle" >/dev/null 2>&1 || true; fi + rm -rf -- "$scratch" +} +trap cleanup EXIT +consumer=$(docker create --entrypoint /bin/true "$image") +docker cp "$consumer:/usr/share/pasturestack/security/ubuntu-backports/." "$evidence/" +docker rm "$consumer" >/dev/null +consumer= +bundle=$(docker create "$bundle_image" /artifact-export-only) +docker cp "$bundle:/out/verify-consumer-evidence.py" "$scratch/verify-consumer-evidence.py" +docker cp "$bundle:/out/SHA256SUMS" "$scratch/bundle-SHA256SUMS" +docker rm "$bundle" >/dev/null +bundle= +python3 -B "$scratch/verify-consumer-evidence.py" \ + --evidence-dir "$evidence" --sbom "$sbom" \ + --expected-bundle-digest "$bundle_digest" \ + --bundle-checksums "$scratch/bundle-SHA256SUMS" diff --git a/.github/scripts/test_buildx_code_absence.py b/.github/scripts/test_buildx_code_absence.py new file mode 100644 index 0000000..deafcc9 --- /dev/null +++ b/.github/scripts/test_buildx_code_absence.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Focused source-bound applicability and native VEX regression controls.""" +import copy +import json +import unittest +from unittest.mock import patch + +import check_buildx_code_absence as guard + + +def finding(cve, purl=guard.PURL): + return {'VulnerabilityID': cve, 'PkgIdentifier': {'PURL': purl}, 'Severity': 'HIGH'} + + +def fixture(): + binding = dict(guard.PINS, goarch='amd64', binarySha256='a' * 64) + modules = {'github.com/docker/docker': 'v28.5.2+incompatible', + 'github.com/moby/go-archive': 'v0.3.0', 'golang.org/x/net': 'v0.60.0'} + before = dict(modules, **{'github.com/moby/go-archive': 'v0.2.0', 'golang.org/x/net': 'v0.59.0'}) + checks = [{'config': {'scanner_name': 'govulncheck', 'scanner_version': 'v1.9.0', + 'scan_mode': 'source', 'scan_level': 'package'}}, + {'SBOM': {'go_version': 'go1.27.3', 'roots': [guard.MAIN], + 'modules': [{'path': k, 'version': v} for k, v in modules.items()]}}] + checks.extend({'osv': {'id': identifier}} for identifier in sorted(guard.IDS)) + # Legacy module presence is expected; imported/called daemon code is not. + checks.append({'finding': {'osv': 'GO-2026-5746', 'trace': [ + {'module': 'github.com/docker/docker', 'version': 'v28.5.2+incompatible'}]}}) + raw = {'Results': [{'Target': guard.TARGET, 'Vulnerabilities': [ + finding(cve) for cve in sorted(guard.CVES)]}, + {'Target': 'usr/bin/other', 'Vulnerabilities': [finding('CVE-OTHER', 'pkg:generic/other@1')]}]} + vex = {'statements': [{'vulnerability': {'name': cve}, 'products': [{'@id': guard.PURL}], + 'status': 'not_affected', 'justification': 'vulnerable_code_not_present'} + for cve in sorted(guard.CVES)]} + return { + 'binding': ''.join(f'{k}\t{v}\n' for k, v in binding.items()), + 'binarySha256': binding['binarySha256'], + 'packages': '\n'.join([guard.MAIN, guard.NAMES, 'github.com/moby/go-archive/compression']) + '\n', + 'files': f'{guard.NAMES}\t{guard.NAME_FILE}\t{guard.NAME_SHA}\n', + 'modulesBefore': ''.join(f'{k} {v}\n' for k, v in before.items()), + 'modulesAfter': ''.join(f'{k} {v}\n' for k, v in modules.items()), + 'buildInfo': 'docker-buildx: go1.27.3\n' + ''.join(f'\tdep\t{k}\t{v}\th1:example\n' for k, v in modules.items()) + + '\tbuild\tGOOS=linux\n\tbuild\tGOARCH=amd64\n\tbuild\tCGO_ENABLED=0\n', + 'version': f"github.com/docker/buildx {guard.PINS['downstreamVersion']} {guard.PINS['sourceCommit']}\n", + 'sourceCheck': '\n'.join(json.dumps(c) for c in checks), + 'osvInputs': {identifier: json.dumps({'id': identifier}) for identifier in guard.IDS}, + 'raw': json.dumps(raw), 'vex': vex, + } + + +class BuildxEvidenceTests(unittest.TestCase): + def setUp(self): + # Small synthetic OSVs keep the tests independent of network/local files. + # Production pins are fixed constants; the CLI has no pin override. + hashes = {k: guard.sha(v) for k, v in fixture()['osvInputs'].items()} + pins = patch.dict(guard.OSV_PINS, hashes, clear=True) + pins.start(); self.addCleanup(pins.stop) + + def test_exact_source_and_module_only_notice_pass(self): + result = guard.source_check(fixture()) + self.assertEqual(result['status'], 'BUILDX_DAEMON_CODE_ABSENCE_VERIFIED') + self.assertEqual(result['scope'], sorted(guard.CVES)) + # Fixed module reports may be absent from scanner output; their exact + # canonical input bytes are independently required and SHA checked. + data = fixture() + data['sourceCheck'] = '\n'.join(json.dumps(x) for x in guard.objects(data['sourceCheck']) + if x.get('osv', {}).get('id') not in {'GO-2026-6253', 'GO-2026-6611'}) + guard.source_check(data) + + def test_wrong_source_and_binary_identity_rejected(self): + for field, old in [('binding', guard.PINS['sourceCommit']), + ('binding', guard.PINS['sourceArchiveSha256']), + ('binding', guard.PINS['updatedGoModSha256']), + ('binarySha256', 'a' * 64), ('buildInfo', 'go1.27.3'), + ('buildInfo', 'GOARCH=amd64'), ('buildInfo', 'v0.60.0')]: + with self.subTest(field=field, old=old): + data = fixture(); data[field] = data[field].replace(old, 'wrong', 1) + with self.assertRaises(ValueError): + guard.source_check(data) + + def test_full_daemon_and_wrong_namesgenerator_source_rejected(self): + for field, value in [('packages', 'github.com/docker/docker/daemon\n'), + ('packages', 'github.com/moby/go-archive\n'), + ('files', f'{guard.NAMES}\tmodule/extra.go\t' + 'b' * 64 + '\n')]: + with self.subTest(value=value): + data = fixture(); data[field] += value + with self.assertRaises(ValueError): + guard.source_check(data) + data = fixture(); data['files'] = data['files'].replace(guard.NAME_SHA, 'b' * 64) + with self.assertRaises(ValueError): + guard.source_check(data) + + def test_other_binary_same_purl_rejected(self): + for cve in ['CVE-2026-41567', 'CVE-UNREVIEWED']: + data = fixture(); raw = json.loads(data['raw']) + raw['Results'].append({'Target': 'usr/bin/daemon', 'Vulnerabilities': [finding(cve)]}) + data['raw'] = json.dumps(raw) + with self.subTest(cve=cve), self.assertRaises(ValueError): + guard.source_check(data) + + def test_unrelated_module_bump_rejected(self): + data = fixture(); data['modulesAfter'] += 'example.org/unrelated v2.0.0\n' + with self.assertRaises(ValueError): + guard.source_check(data) + + def test_incomplete_or_other_program_source_scan_rejected(self): + for old, new in [('"source"', '"binary"'), ('"package"', '"module"'), + ('"SBOM"', '"sbom"'), + (guard.MAIN, 'example.org/other'), ('go1.27.3', 'go1.26.10'), + ('GO-2026-6253', 'GO-MISSING')]: + data = fixture(); data['sourceCheck'] = data['sourceCheck'].replace(old, new) + with self.subTest(old=old), self.assertRaises(ValueError): + guard.source_check(data) + for tamper in ['missing-input', 'changed-input', 'missing-daemon-output']: + data = fixture() + if tamper == 'missing-input': data['osvInputs'].pop('GO-2026-6253') + if tamper == 'changed-input': data['osvInputs']['GO-2026-6611'] += '\n' + if tamper == 'missing-daemon-output': + data['sourceCheck'] = '\n'.join(json.dumps(x) for x in guard.objects(data['sourceCheck']) + if x.get('osv', {}).get('id') != 'GO-2026-5746') + with self.subTest(tamper=tamper), self.assertRaises(ValueError): + guard.source_check(data) + + def test_imported_or_called_vulnerable_code_rejected(self): + for frame in [{'module': 'github.com/docker/docker', 'package': 'github.com/docker/docker/daemon'}, + {'module': 'github.com/docker/docker', 'function': 'Daemon.openContainerFS'}]: + data = fixture(); data['sourceCheck'] += '\n' + json.dumps( + {'finding': {'osv': 'GO-2026-5617', 'trace': [frame]}}) + with self.subTest(frame=frame), self.assertRaises(ValueError): + guard.source_check(data) + + def test_vex_cannot_extend_legacy_module_scope(self): + data = fixture(); statement = copy.deepcopy(data['vex']['statements'][0]) + statement['vulnerability']['name'] = 'CVE-UNREVIEWED' + data['vex']['statements'].append(statement) + with self.assertRaises(ValueError): + guard.source_check(data) + + def test_native_vex_keeps_complete_unrelated_finding(self): + data = fixture(); data['guard'] = guard.source_check(data) + effective = json.loads(data['raw']); effective['Results'][0]['Vulnerabilities'] = [] + data['effective'] = json.dumps(effective) + self.assertTrue(guard.native_check(data)['rawRetainedUnchanged']) + self.assertEqual(guard.native_check(data)['retainedFindings'], 1) + + def test_native_tamper_or_unrelated_removal_rejected(self): + for tamper in ['raw', 'vex', 'remove', 'alter', 'retain']: + data = fixture(); data['guard'] = guard.source_check(data) + effective = json.loads(data['raw']); effective['Results'][0]['Vulnerabilities'] = [] + if tamper == 'raw': data['raw'] += '\n' + if tamper == 'vex': data['vex']['statements'].pop() + if tamper == 'remove': effective['Results'][1]['Vulnerabilities'] = [] + if tamper == 'alter': effective['Results'][1]['Vulnerabilities'][0]['Severity'] = 'LOW' + if tamper == 'retain': effective = json.loads(data['raw']) + data['effective'] = json.dumps(effective) + with self.subTest(tamper=tamper), self.assertRaises(ValueError): + guard.native_check(data) + + +if __name__ == '__main__': + unittest.main() diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index b0f0b6c..161ca93 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -19,9 +19,9 @@ jobs: timeout-minutes: 75 env: DAPPER_IMAGE: pasturestack/container-cron-dapper:${{ github.sha }} - RUNTIME_IMAGE: pasturestack/container-cron:v0.6.1 - TRIVY_IMAGE: aquasec/trivy:0.73.0@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c - VERSION_OVERRIDE: v0.6.1 + RUNTIME_IMAGE: pasturestack/container-cron:v0.6.2 + TRIVY_IMAGE: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 + VERSION_OVERRIDE: v0.6.2 steps: - name: Check out candidate @@ -39,6 +39,8 @@ jobs: git rev-parse HEAD > evidence/source-revision.txt sha256sum \ Dockerfile.dapper \ + toolchain/build-buildx \ + .github/scripts/check_buildx_code_absence.py \ package/Dockerfile \ scripts/package \ security/dapper.openvex.json \ @@ -47,6 +49,9 @@ jobs: go.sum \ > evidence/source-locks.sha256 + - name: Validate Buildx source applicability guards + run: python3 -B .github/scripts/test_buildx_code_absence.py + - name: Build, test, validate, and package shell: bash run: | @@ -64,11 +69,11 @@ jobs: --env ARCH=amd64 \ --env "VERSION_OVERRIDE=$VERSION_OVERRIDE" \ --env "IMAGE_NAME=pasturestack/container-cron" \ - --env TAG=v0.6.1 \ + --env TAG=v0.6.2 \ "$DAPPER_IMAGE" ci test -x bin/container-cron test -L bin/container-crontab - bin/container-cron --version | grep -F 'v0.6.1' >/dev/null + bin/container-cron --version | grep -F 'v0.6.2' >/dev/null test "$(cat dist/images)" = "$RUNTIME_IMAGE" docker image inspect "$RUNTIME_IMAGE" >/dev/null @@ -76,8 +81,8 @@ jobs: shell: bash run: | set -euo pipefail - VERSION_OVERRIDE=v0.6.1 \ - TAG=v0.6.1 \ + VERSION_OVERRIDE=v0.6.2 \ + TAG=v0.6.2 \ IMAGE_NAME=pasturestack/container-cron \ ./scripts/smoke @@ -104,6 +109,11 @@ jobs: grep -F 'Docker version 29.9.2' evidence/docker-cli-version.txt >/dev/null docker run --rm --entrypoint sh "$DAPPER_IMAGE" -lc \ 'test ! -e /usr/libexec/docker/cli-plugins/docker-buildx' + docker run --rm --entrypoint go "$DAPPER_IMAGE" \ + version -m /usr/local/lib/docker/cli-plugins/docker-buildx \ + > evidence/buildx-go-version.txt + docker run --rm --entrypoint docker "$DAPPER_IMAGE" buildx version \ + > evidence/buildx-version.txt sha256sum bin/container-cron > evidence/container-cron.sha256 - name: Scan source, product, and images @@ -154,6 +164,24 @@ jobs: -v "$trivy_cache:/root/.cache/trivy" \ "$TRIVY_IMAGE" image \ --skip-db-update --offline-scan --scanners vuln,secret --format json \ + --output /work/evidence/runtime-security-raw.json "$RUNTIME_IMAGE" + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD:/work" -w /work \ + -v "$trivy_cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" image --skip-db-update --offline-scan --format cyclonedx \ + --output /work/evidence/runtime.cdx.json "$RUNTIME_IMAGE" + backports="$RUNNER_TEMP/container-cron-ubuntu-backports" + bash .github/scripts/collect-ubuntu-backport-evidence.sh \ + "$RUNTIME_IMAGE" "evidence/runtime.cdx.json" "$backports" \ + > "evidence/ubuntu-backports-verification.json" + tar -C "$backports" -czf "evidence/ubuntu-backports.tar.gz" . + docker run --rm -v "$backports:/backports:ro" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v "$PWD:/work" -w /work \ + -v "$trivy_cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" image --vex /backports/openvex.json \ + --skip-db-update --offline-scan --scanners vuln,secret --format json \ --output /work/evidence/runtime-security.json "$RUNTIME_IMAGE" docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ @@ -181,6 +209,47 @@ jobs: --skip-db-update --offline-scan --scanners vuln,secret --format json \ --output /evidence/dapper-rootfs-raw.json /scan + # The daemon statements qualify this exact tool only. Refuse another + # binary with the same PURL before the native matcher sees the VEX. + cp "$dapper_rootfs"/licenses/BUILDX-* evidence/ + buildx_sha="$(sha256sum "$dapper_rootfs/usr/local/lib/docker/cli-plugins/docker-buildx")" + buildx_sha="${buildx_sha%% *}" + jq -n \ + --rawfile binding evidence/BUILDX-SOURCE.tsv \ + --rawfile packages evidence/BUILDX-PACKAGES.txt \ + --rawfile files evidence/BUILDX-SOURCE-FILES.tsv \ + --rawfile modulesBefore evidence/BUILDX-MODULES-before.txt \ + --rawfile modulesAfter evidence/BUILDX-MODULES-after.txt \ + --rawfile buildInfo evidence/buildx-go-version.txt \ + --rawfile version evidence/buildx-version.txt \ + --rawfile sourceCheck evidence/BUILDX-SOURCE-GOVULNCHECK.json \ + --rawfile daemon42306 evidence/BUILDX-GO-2026-5617.json \ + --rawfile daemon41567 evidence/BUILDX-GO-2026-5746.json \ + --rawfile archive17106 evidence/BUILDX-GO-2026-6253.json \ + --rawfile net78669 evidence/BUILDX-GO-2026-6611.json \ + --rawfile raw evidence/dapper-rootfs-raw.json \ + --slurpfile vex security/dapper.openvex.json \ + --arg binarySha256 "$buildx_sha" \ + '{binding:$binding,packages:$packages,files:$files,modulesBefore:$modulesBefore,modulesAfter:$modulesAfter,buildInfo:$buildInfo,version:$version,sourceCheck:$sourceCheck,osvInputs:{"GO-2026-5617":$daemon42306,"GO-2026-5746":$daemon41567,"GO-2026-6253":$archive17106,"GO-2026-6611":$net78669},raw:$raw,vex:$vex[0],binarySha256:$binarySha256}' \ + | python3 -B .github/scripts/check_buildx_code_absence.py \ + > evidence/buildx-code-absence.json + + docker run --rm \ + -v "$dapper_rootfs:/scan:ro" \ + -v "$PWD/evidence:/evidence" \ + -v "$PWD/security:/vex:ro" \ + -v "$trivy_cache:/root/.cache/trivy" \ + "$TRIVY_IMAGE" rootfs --vex /vex/dapper.openvex.json \ + --skip-db-update --offline-scan --scanners vuln,secret --format json \ + --output /evidence/dapper-rootfs-effective.json /scan + jq -n --rawfile raw evidence/dapper-rootfs-raw.json \ + --rawfile effective evidence/dapper-rootfs-effective.json \ + --slurpfile guard evidence/buildx-code-absence.json \ + --slurpfile vex security/dapper.openvex.json \ + '{raw:$raw,effective:$effective,guard:$guard[0],vex:$vex[0]}' \ + | python3 -B .github/scripts/check_buildx_code_absence.py --native \ + > evidence/dapper-native-vex.json + jq -r ' .Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" or .Severity == "HIGH") @@ -238,12 +307,6 @@ jobs: -v "$trivy_cache:/root/.cache/trivy" \ "$TRIVY_IMAGE" rootfs --skip-db-update --offline-scan --format cyclonedx \ --output /evidence/product.cdx.json /scan - docker run --rm \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v "$PWD:/work" -w /work \ - -v "$trivy_cache:/root/.cache/trivy" \ - "$TRIVY_IMAGE" image --skip-db-update --offline-scan --format cyclonedx \ - --output /work/evidence/runtime.cdx.json "$RUNTIME_IMAGE" docker run --rm \ -v "$dapper_rootfs:/scan:ro" \ -v "$PWD/evidence:/evidence" \ @@ -319,9 +382,10 @@ jobs: mkdir -p dist/security-runtime docker image inspect "$RUNTIME_IMAGE" > dist/security-runtime/images.json docker save "$RUNTIME_IMAGE" | gzip -n > dist/security-runtime/images.tar.gz - cp evidence/runtime-security.json evidence/runtime.cdx.json dist/security-runtime/ + cp evidence/runtime-security-raw.json evidence/runtime-security.json evidence/runtime.cdx.json \ + evidence/ubuntu-backports-verification.json evidence/ubuntu-backports.tar.gz dist/security-runtime/ printf '%s\n' "$GITHUB_SHA" > dist/security-runtime/source-revision.txt - (cd dist/security-runtime && sha256sum -- *.json images.tar.gz source-revision.txt > SHA256SUMS) + (cd dist/security-runtime && sha256sum -- *.json *.tar.gz source-revision.txt > SHA256SUMS) - name: Retain verified runtime for immutable publication uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -359,7 +423,7 @@ jobs: packages: write env: GH_TOKEN: ${{ github.token }} - RELEASE_TAG: v0.6.1 + RELEASE_TAG: v0.6.2 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -378,7 +442,7 @@ jobs: printf '%s' "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin trap 'docker logout ghcr.io >/dev/null 2>&1 || true' EXIT : > dist/security-runtime/registry-images.jsonl - local_image='pasturestack/container-cron:v0.6.1' + local_image='pasturestack/container-cron:v0.6.2' remote_image="ghcr.io/pasturestack/container-cron:$RELEASE_TAG" if docker manifest inspect "$remote_image" >/dev/null 2>"$RUNNER_TEMP/manifest-error"; then echo "Refusing to replace an existing immutable image tag: $remote_image" >&2; exit 1 diff --git a/Dockerfile.dapper b/Dockerfile.dapper index 6a5abfb..4a75ce5 100644 --- a/Dockerfile.dapper +++ b/Dockerfile.dapper @@ -3,6 +3,7 @@ FROM ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea264 COPY ubuntu-apt.lock /licenses/ubuntu-apt.lock COPY ubuntu-security-packages.lock /licenses/ubuntu-security-packages.lock COPY scripts/install-ubuntu-security-updates /usr/local/libexec/pasturestack/install-ubuntu-security-updates +COPY toolchain/build-buildx /usr/local/libexec/pasturestack/build-buildx ADD --checksum=sha256:5b6326f1932cfd7be57130561dc78192f09ed9f58bfbe6606ed0c50510c3b6cd https://archive.ubuntu.com/ubuntu/pool/main/c/ca-certificates/ca-certificates_20260816ubuntu0.26.04.1_all.deb /tmp/ca-certificates.deb ARG DAPPER_HOST_ARCH=amd64 @@ -85,6 +86,8 @@ RUN case "${DAPPER_HOST_ARCH}" in \ go version && \ docker --version +RUN bash /usr/local/libexec/pasturestack/build-buildx + ENV DAPPER_SOURCE=/go/src/github.com/PastureStack/container-cron/ \ DAPPER_OUTPUT="./bin ./dist" \ DAPPER_DOCKER_SOCKET=true \ diff --git a/README.md b/README.md index 8c931a0..58c404d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # PastureStack Container Cron +Linux runtime **`v0.6.2`** consumes verified Ubuntu security backports without adding packages or changing the existing coreutils provider. + +The runtime recipe applies the qualified Ubuntu 26.04 backport bundle only to already-installed packages and preserves the selected coreutils provider. The immutable bundle digest and final-image checks are pending. Bundle manifests, qualification, exact-version OpenVEX, source inputs and install/verify receipts are retained under `/usr/share/pasturestack/security/ubuntu-backports/`; raw scan evidence and the existing full-severity release policy remain required. + Container Cron watches Docker events and runs `start`, `stop`, or `restart` actions according to cron expressions stored in container labels. The maintained runtime targets the PastureStack 1.6 compatibility control plane and current Docker Engine releases. -The current public release is [`v0.6.1`](https://github.com/PastureStack/container-cron/releases/tag/v0.6.1), produced from `2cce2cc80f2590239d5ef758a21c54a3f1e0f2b9` by the [successful release gate](https://github.com/PastureStack/container-cron/actions/runs/39960497907). The published `ghcr.io/pasturestack/container-cron:v0.6.1` digest is `sha256:ea2790f2a3e32eb6e3266eb175ee0a69c4a854463c22a74604f69855da5c6829`. Later source or documentation changes do not alter that producer; component publication is not Server deployment. - ## Build and test The supported development workflow uses the repository's disposable Dapper @@ -23,7 +25,7 @@ legacy `bin/container-crontab` executable alias for staged upgrades. Run the isolated container lifecycle test after packaging: ```sh -VERSION_OVERRIDE=v0.6.1 TAG=v0.6.1 make smoke +VERSION_OVERRIDE=v0.6.2 TAG=v0.6.2 make smoke ``` The smoke test creates only uniquely named disposable containers and verifies @@ -31,6 +33,8 @@ that scheduled `start` and `stop` actions both reach the Docker daemon. ## Run +The runtime image is `ghcr.io/pasturestack/container-cron:v0.6.2`. + Standard Docker mode: ```sh @@ -103,5 +107,4 @@ finding with an official fix blocks release, regardless of severity. Unknown findings block unless a separately reviewed, exact not-affected statement is proved by the actual product dependency graph. LOW/MEDIUM findings without an official fix require the repository's exact CVE/package/version/PURL review ledger; -new or changed findings fail closed. This policy-only update does not replace any -already published immutable artifact. +new or changed findings fail closed. Each candidate must pass this policy against its exact image and package versions. diff --git a/package/Dockerfile b/package/Dockerfile index 9ffd4c4..631fb61 100644 --- a/package/Dockerfile +++ b/package/Dockerfile @@ -1,4 +1,10 @@ +# syntax=docker/dockerfile:1.7 +ARG UBUNTU_SECURITY_BACKPORTS_IMAGE=ghcr.io/pasturestack/ubuntu-security-backports:v0.1.0@sha256:2742a3d429e4e14bc587f6dedc51ded4246cf90bbaedcd86c67d0885e32a6f2a + +FROM ${UBUNTU_SECURITY_BACKPORTS_IMAGE} AS ubuntu_security_backports + FROM ubuntu:26.04@sha256:2260313b31c8c011cd2eebe728008efac1b3982be73eb71348ea2648d2c0e09b +ARG UBUNTU_SECURITY_BACKPORTS_IMAGE ARG VERSION ARG REVISION @@ -41,6 +47,24 @@ RUN set -eux; \ apt-get clean; \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/bin/pebble +# Keep the build-only installer and package archives outside the final image. +RUN --mount=type=bind,from=ubuntu_security_backports,source=/out,target=/opt/pasturestack/ubuntu-security-backports,readonly \ + set -eux; \ + evidence=/usr/share/pasturestack/security/ubuntu-backports; \ + mkdir -p "$evidence"; \ + /opt/pasturestack/ubuntu-security-backports/install-existing-backports --install-in-docker-build > "$evidence/install.json"; \ + /opt/pasturestack/ubuntu-security-backports/install-existing-backports --verify-installed > "$evidence/verify.json"; \ + for file in manifest.json qualification.json source-inputs.lock.json openvex.json; do \ + cp "/opt/pasturestack/ubuntu-security-backports/$file" "$evidence/$file"; \ + done; \ + printf '%s\n' "$UBUNTU_SECURITY_BACKPORTS_IMAGE" > "$evidence/bundle-image.txt"; \ + (cd "$evidence" && sha256sum manifest.json qualification.json source-inputs.lock.json openvex.json install.json verify.json bundle-image.txt > SHA256SUMS); \ + . /licenses/ubuntu-apt.lock; \ + { \ + printf 'snapshot\t%s\n' "$UBUNTU_APT_SNAPSHOT"; \ + dpkg-query -W -f='${binary:Package}\t${Version}\n' | LC_ALL=C sort; \ + } > /licenses/CONTAINER-CRON-RUNTIME-UBUNTU-APT-PACKAGES.tsv + COPY container-cron /usr/bin/container-cron COPY LICENSE SECURITY.md UNOFFICIAL_MAINTENANCE_FORK.md ORIGIN.md COMPATIBILITY.md /licenses/ RUN chmod +x /usr/bin/container-cron && \ diff --git a/scripts/package b/scripts/package index 5253de8..7bcfe5e 100755 --- a/scripts/package +++ b/scripts/package @@ -36,7 +36,7 @@ docker_build_network_arg=() if [ -n "${DOCKER_BUILD_NETWORK:-}" ]; then docker_build_network_arg=(--network="${DOCKER_BUILD_NETWORK}") fi -DOCKER_BUILDKIT=0 docker build "${docker_build_network_arg[@]}" \ +DOCKER_BUILDKIT=1 docker build "${docker_build_network_arg[@]}" \ --build-arg VERSION="${TAG}" \ --build-arg REVISION="${REVISION}" \ -t "${IMAGE_NAME}:${TAG}" "${build_context}" diff --git a/security/dapper.openvex.json b/security/dapper.openvex.json index 7004a52..1746158 100644 --- a/security/dapper.openvex.json +++ b/security/dapper.openvex.json @@ -2,9 +2,23 @@ "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://github.com/PastureStack/container-cron/security/openvex/dapper/2026-10-23", "author": "PastureStack contributors", - "timestamp": "2026-10-23T13:00:00Z", - "version": 2, + "timestamp": "2026-10-25T12:00:00Z", + "version": 3, "statements": [ + { + "vulnerability": {"name": "CVE-2026-41567"}, + "products": [{"@id": "pkg:golang/github.com/docker/docker@v28.5.2%2Bincompatible"}], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Limited to docker-buildx v0.38.1-pasturestack.1 in this Dapper filesystem. The unmodified official Buildx source commit 0dbe87f36ed472fe36b6796c3f70b4210a731fc5 (archive SHA256 c5c810b083ed3725f7d4299ba01505b5891d164cf81c3398ea4c3b27a3b71149), built with Go 1.27.3 and exactly x/net v0.60.0/go-archive v0.3.0, compiles only pkg/namesgenerator from the legacy Docker module. The affected daemon containerExtractToDir implementation is absent. Before applying native VEX the gate verifies actual binary identity, resolved compiled packages/files and official namesgenerator hash, native source govulncheck, and that no other binary contains this PURL. Original complete raw findings remain retained; a different source, daemon package or other binary fails qualification. This statement does not apply generally to the Docker module or daemon." + }, + { + "vulnerability": {"name": "CVE-2026-42306"}, + "products": [{"@id": "pkg:golang/github.com/docker/docker@v28.5.2%2Bincompatible"}], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Limited to docker-buildx v0.38.1-pasturestack.1 in this Dapper filesystem. The unmodified official Buildx source commit 0dbe87f36ed472fe36b6796c3f70b4210a731fc5 (archive SHA256 c5c810b083ed3725f7d4299ba01505b5891d164cf81c3398ea4c3b27a3b71149), built with Go 1.27.3 and exactly x/net v0.60.0/go-archive v0.3.0, compiles only pkg/namesgenerator from the legacy Docker module. The affected daemon openContainerFS implementation is absent. Before applying native VEX the gate verifies actual binary identity, resolved compiled packages/files and official namesgenerator hash, native source govulncheck, and that no other binary contains this PURL. Original complete raw findings remain retained; a different source, daemon package or other binary fails qualification. This statement does not apply generally to the Docker module or daemon." + }, { "vulnerability": { "name": "CVE-2022-50240" diff --git a/toolchain/build-buildx b/toolchain/build-buildx new file mode 100644 index 0000000..565b4cd --- /dev/null +++ b/toolchain/build-buildx @@ -0,0 +1,93 @@ +#!/bin/bash +set -euo pipefail + +# Unmodified official source; only these two accepted module releases change. +source_commit=0dbe87f36ed472fe36b6796c3f70b4210a731fc5 +source_sha=c5c810b083ed3725f7d4299ba01505b5891d164cf81c3398ea4c3b27a3b71149 +original_mod_sha=a93029abb94e88ddaa532ddeaab4802259a1025408f1eac417a9886e140067ef +updated_mod_sha=230c98fa66a534b3a4d2dcdb0507e871658deff3c80058b01d85d7a18ce656b9 +version=v0.38.1-pasturestack.1 +source_dir=/tmp/pasturestack-buildx-source +export GOMODCACHE=/tmp/pasturestack-buildx-mod GOCACHE=/tmp/pasturestack-buildx-cache +export GOFLAGS=-mod=mod GOWORK=off GOTOOLCHAIN=local CGO_ENABLED=0 +export GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org +export GOOS=linux GOARCH="${DAPPER_HOST_ARCH:?}" +case "$GOARCH" in amd64|arm64) ;; *) exit 1 ;; esac +test "$(go env GOVERSION)" = go1.27.3 +test ! -e "$source_dir" +curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \ + --retry 5 --retry-all-errors -o /tmp/pasturestack-buildx.tar.gz \ + "https://codeload.github.com/docker/buildx/tar.gz/${source_commit}" +echo "$source_sha /tmp/pasturestack-buildx.tar.gz" | sha256sum -c - +mkdir -p "$source_dir" /usr/local/lib/docker/cli-plugins /licenses +tar -xzf /tmp/pasturestack-buildx.tar.gz --strip-components=1 -C "$source_dir" +cd "$source_dir" +echo "$original_mod_sha go.mod" | sha256sum -c - +go list -m all > /licenses/BUILDX-MODULES-before.txt +go mod edit -require=golang.org/x/net@v0.60.0 -require=github.com/moby/go-archive@v0.3.0 +echo "$updated_mod_sha go.mod" | sha256sum -c - +go mod download +go mod verify +go list -deps ./cmd/buildx > /licenses/BUILDX-PACKAGES.txt +go list -deps -compiled -f '{{ $p := . }}{{range .CompiledGoFiles}}{{printf "%s\t%s\t%s\n" $p.ImportPath $p.Dir .}}{{end}}{{range .SFiles}}{{printf "%s\t%s\t%s\n" $p.ImportPath $p.Dir .}}{{end}}{{range .HFiles}}{{printf "%s\t%s\t%s\n" $p.ImportPath $p.Dir .}}{{end}}{{range .EmbedFiles}}{{printf "%s\t%s\t%s\n" $p.ImportPath $p.Dir .}}{{end}}' \ + ./cmd/buildx > /tmp/pasturestack-buildx-files.txt +while IFS=$'\t' read -r package directory name; do + [ -n "$package" ] || continue + case "$name" in /*) file="$name" ;; *) file="$directory/$name" ;; esac + test -f "$file" && test ! -L "$file" + case "$file" in + "$source_dir"/*) logical="source/${file#"$source_dir"/}" ;; + "$GOMODCACHE"/*) logical="module/${file#"$GOMODCACHE"/}" ;; + /usr/local/go/src/*) logical="goroot/${file#/usr/local/go/src/}" ;; + *) printf 'Unexpected compiled source path: %s\n' "$file" >&2; exit 1 ;; + esac + hash="$(sha256sum -- "$file")"; hash="${hash%% *}" + printf '%s\t%s\t%s\n' "$package" "$logical" "$hash" +done < /tmp/pasturestack-buildx-files.txt | LC_ALL=C sort -u > /licenses/BUILDX-SOURCE-FILES.tsv + +# Keep the canonical four reports byte-for-byte. This is a bounded source check; +# the existing complete Trivy scans still enforce all of the original policy. +db=/tmp/pasturestack-buildx-vulndb +mkdir -p "$db/ID" "$db/index" /tmp/pasturestack-buildx-tools +while read -r id hash; do + curl --fail --silent --show-error --proto '=https' --retry 5 --retry-all-errors \ + -o "$db/ID/$id.json" "https://vuln.go.dev/ID/$id.json" + echo "$hash $db/ID/$id.json" | sha256sum -c - + cp "$db/ID/$id.json" "/licenses/BUILDX-$id.json" +done <<'PINS' +GO-2026-5617 5a24060e5cc8cbf6a146615ebe5a7e5beac2f8f267c905538736ed6931ae2af5 +GO-2026-5746 d11da88232883a96ac3b74f848f172ce86c3783b7818d7da4b925335621d5273 +GO-2026-6253 4f337e5aad0f8e83e3db2fc67d058fe0e80017b8c68f494b7e82be87e7120872 +GO-2026-6611 5e89ac4314a24eac41111b71ef4544f22836c7b1baf48747e31c5c1d8282c3de +PINS +printf '%s\n' '{"modified":"2026-10-08T22:31:09Z"}' > "$db/index/db.json" +printf '%s\n' '[{"id":"GO-2026-6253","modified":"2026-08-25T19:43:10Z","aliases":["CVE-2026-17106","GHSA-hfg8-hc9c-6c3h"]},{"id":"GO-2026-5746","modified":"2026-07-23T17:09:44Z","aliases":["CVE-2026-41567","GHSA-x86f-5xw2-fm2r"]},{"id":"GO-2026-5617","modified":"2026-07-27T20:14:16Z","aliases":["CVE-2026-42306","GHSA-rg2x-37c3-w2rh"]},{"id":"GO-2026-6611","modified":"2026-10-08T22:31:09Z","aliases":["CVE-2026-78669"]}]' > "$db/index/vulns.json" +printf '%s\n' '[{"path":"github.com/docker/docker","vulns":[{"id":"GO-2026-5617","modified":"2026-07-27T20:14:16Z"},{"id":"GO-2026-5746","modified":"2026-07-23T17:09:44Z"}]},{"path":"github.com/moby/moby","vulns":[{"id":"GO-2026-5617","modified":"2026-07-27T20:14:16Z"},{"id":"GO-2026-5746","modified":"2026-07-23T17:09:44Z"}]},{"path":"github.com/moby/moby/v2","vulns":[{"id":"GO-2026-5617","modified":"2026-07-27T20:14:16Z"},{"id":"GO-2026-5746","modified":"2026-07-23T17:09:44Z"}]},{"path":"github.com/moby/go-archive","vulns":[{"id":"GO-2026-6253","modified":"2026-08-25T19:43:10Z"}]},{"path":"golang.org/x/net","vulns":[{"id":"GO-2026-6611","modified":"2026-10-08T22:31:09Z"}]},{"path":"stdlib","vulns":[{"id":"GO-2026-6611","modified":"2026-10-08T22:31:09Z"}]}]' > "$db/index/modules.json" +GOBIN=/tmp/pasturestack-buildx-tools go install golang.org/x/vuln/cmd/govulncheck@v1.9.0 +# The vulnerable daemon package must be absent, so package-level source +# analysis is sufficient and avoids an unnecessary whole-Buildx call graph. +GOPROXY=off /tmp/pasturestack-buildx-tools/govulncheck -mode=source -scan=package -format=json \ + -db="file://$db" ./cmd/buildx > /licenses/BUILDX-SOURCE-GOVULNCHECK.json +GOPROXY=off go build -trimpath -buildvcs=false \ + -ldflags "-s -w -buildid= -X github.com/docker/buildx/version.Package=github.com/docker/buildx -X github.com/docker/buildx/version.Version=${version} -X github.com/docker/buildx/version.Revision=${source_commit}" \ + -o /usr/local/lib/docker/cli-plugins/docker-buildx ./cmd/buildx +echo "$updated_mod_sha go.mod" | sha256sum -c - +go list -m all > /licenses/BUILDX-MODULES-after.txt +sed -E '/^(golang.org\/x\/net|github.com\/moby\/go-archive) /d' /licenses/BUILDX-MODULES-before.txt > /tmp/pasturestack-buildx-before-other.txt +sed -E '/^(golang.org\/x\/net|github.com\/moby\/go-archive) /d' /licenses/BUILDX-MODULES-after.txt > /tmp/pasturestack-buildx-after-other.txt +git diff --no-index --exit-code /tmp/pasturestack-buildx-before-other.txt /tmp/pasturestack-buildx-after-other.txt +grep -Fx 'golang.org/x/net v0.60.0' /licenses/BUILDX-MODULES-after.txt +grep -Fx 'github.com/moby/go-archive v0.3.0' /licenses/BUILDX-MODULES-after.txt +go version -m /usr/local/lib/docker/cli-plugins/docker-buildx > /licenses/BUILDX-GO-VERSION.txt +docker buildx version > /licenses/BUILDX-VERSION.txt +grep -F "github.com/docker/buildx $version $source_commit" /licenses/BUILDX-VERSION.txt +binary_sha="$(sha256sum /usr/local/lib/docker/cli-plugins/docker-buildx)"; binary_sha="${binary_sha%% *}" +printf '%s\t%s\n' schemaVersion 1 upstreamVersion v0.38.1 downstreamVersion "$version" \ + sourceCommit "$source_commit" sourceArchiveSha256 "$source_sha" \ + originalGoModSha256 "$original_mod_sha" updatedGoModSha256 "$updated_mod_sha" \ + goVersion go1.27.3 goos linux goarch "$GOARCH" binarySha256 "$binary_sha" \ + > /licenses/BUILDX-SOURCE.tsv +cd / +rm -rf /tmp/pasturestack-buildx-source /tmp/pasturestack-buildx-mod /tmp/pasturestack-buildx-cache \ + /tmp/pasturestack-buildx-vulndb /tmp/pasturestack-buildx-tools /tmp/pasturestack-buildx.tar.gz \ + /tmp/pasturestack-buildx-files.txt /tmp/pasturestack-buildx-before-other.txt /tmp/pasturestack-buildx-after-other.txt