From 194f38b2d9582110d14b21b67e48ed13b58152f9 Mon Sep 17 00:00:00 2001 From: Andrii Klymenko Date: Tue, 21 Apr 2026 18:13:18 +0300 Subject: T8542: Add functionality to generate SBOM file from ISO image Use the Syft util to generate SBOM files in both CycloneDX and SPDX format. This process includes creating a transfer disk to copy the Syft util from the host machine and copy the scan results back to the host machine. --- scripts/check-qemu-install | 111 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/check-qemu-install b/scripts/check-qemu-install index 9a9b6042..f850a8cb 100755 --- a/scripts/check-qemu-install +++ b/scripts/check-qemu-install @@ -123,6 +123,10 @@ parser.add_argument('--tpmtest', help='Execute TPM encrypted config tests', action='store_true', default=False) parser.add_argument('--sbtest', help='Execute Secure Boot tests', action='store_true', default=False) +parser.add_argument('--sbom', help='Generate SBOM file using syft', + action='store_true', default=False) +parser.add_argument('--sbom-output-dir', help='Host directory to mount and copy SBOM file into', + type=str, default=None) parser.add_argument('--cloud-init', help='Execute cloud-init tests', action='store_true', default=False) parser.add_argument('--qemu-cmd', help='Only generate QEMU launch command', @@ -140,6 +144,12 @@ parser.add_argument('--isolate-cpus', help='CPU cores to isolate (e.g., 1,3-4', args = parser.parse_args() +if args.sbom and not args.sbom_output_dir: + args.sbom_output_dir = os.getcwd() + +if args.sbom_output_dir: + os.makedirs(args.sbom_output_dir, exist_ok=True) + if args.cloud_init: hostname = CI_CONFIG_ARGS['hostname'] op_mode_prompt = rf'vyos@{hostname}:~\$' @@ -185,7 +195,7 @@ class EarlyExit(Exception): def _kvm_exists(): return os.path.exists("/dev/kvm") -def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False, vnc_enabled=False, secure_boot=False): +def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False, vnc_enabled=False, secure_boot=False, transfer_disk=None): uefi = "" uuid = "f48b60b2-e6ad-49ef-9d09-4245d0585e52" accel = ',accel=kvm' if _kvm_exists() else '' @@ -271,6 +281,10 @@ def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False ' -tpmdev emulator,id=tpm0,chardev=chrtpm' \ ' -device tpm-tis,tpmdev=tpm0' + if transfer_disk: + cmd += f' -drive format=raw,file={transfer_disk},if=none,media=disk,id=drive-transfer,readonly=off' \ + f' -device scsi-hd,bus=scsi0.0,drive=drive-transfer,id=transfer-disk' + return cmd def shutdownVM(c, log, message=''): @@ -375,6 +389,30 @@ if args.raid: # must be called after the raid disk as args.disk name is altered in the RAID path gen_disk(args.disk) +# Create 512MB transfer disk for SBOM generation if requested, download the syft util to use inside the VM for SBOM generation process. +# It will be used to copy generated SBOM files from the VM to the host via the mounted transfer disk. +transfer_disk_path = None +if args.sbom: + _ts = datetime.now().strftime('%Y%m%d-%H%M%S') + _rand = '%04x' % random.randint(0, 0xFFFF) + transfer_disk_path = f'/tmp/vyos-sbom-transfer-{_ts}-{_rand}.img' + subprocess.check_output(['dd', 'if=/dev/zero', f'of={transfer_disk_path}', 'bs=1M', 'count=512']) + subprocess.check_output(['mkfs.vfat', '-F', '32', '-n', 'SBOMXFER', transfer_disk_path]) + log.info(f'Created SBOM transfer disk: {transfer_disk_path}') + setup_mount = f'{transfer_disk_path}.setup' + os.makedirs(setup_mount, exist_ok=True) + subprocess.check_output(['mount', '-o', 'loop', transfer_disk_path, setup_mount]) + try: + log.info('Downloading syft to transfer disk') + subprocess.check_call( + f'curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b {setup_mount}', + shell=True + ) + log.info('Syft downloaded to transfer disk') + finally: + subprocess.check_output(['umount', setup_mount]) + os.rmdir(setup_mount) + # Create software emulated TPM def start_swtpm(): if not os.path.exists(tpm_folder): @@ -621,7 +659,7 @@ try: # Installing image to disk ################################################# log.info('Installing system') - cmd = get_qemu_cmd(qemu_name, args.uefi, args.disk, raid=diskname_raid, tpm=args.tpmtest, iso_img=args.iso, vnc_enabled=args.vnc, secure_boot=args.sbtest) + cmd = get_qemu_cmd(qemu_name, args.uefi, args.disk, raid=diskname_raid, tpm=args.tpmtest, iso_img=args.iso, vnc_enabled=args.vnc, secure_boot=args.sbtest, transfer_disk=transfer_disk_path) log.debug(f'Executing command: {cmd}') c = pexpect.spawn(cmd, logfile=stl, timeout=60) @@ -1256,6 +1294,59 @@ try: tmp = 'Configtest failed :/ - check debug output' log.error(tmp) raise Exception(tmp) + elif args.sbom: + # Determine SBOM output file names based on ISO name + iso_real = os.path.realpath(args.iso) if args.iso else None + iso_name = os.path.splitext(os.path.basename(iso_real))[0] if iso_real else 'vyos' + sbom_file_cdx = f'{iso_name}.iso.cdx.json' + sbom_file_spdx = f'{iso_name}.iso.spdx.json' + + # Create a mount point for the SBOM transfer disk + log.info('Mounting transfer disk in VM') + c.sendline('sudo mkdir -p /mnt/sbom_transfer') + c.expect(op_mode_prompt) + + c.sendline('sudo mount $(sudo blkid -L SBOMXFER) /mnt/sbom_transfer') + c.expect(op_mode_prompt) + + c.sendline('mountpoint -q /mnt/sbom_transfer && echo MOUNT_OK || echo MOUNT_FAIL') + i = c.expect(['MOUNT_OK', 'MOUNT_FAIL']) + if i != 0: + raise Exception('Failed to mount SBOM transfer disk inside VM') + c.expect(op_mode_prompt) + + # Copy the syft binary to the transfer disk and check the version to verify it works before running the SBOM generation process + c.sendline('sudo cp /mnt/sbom_transfer/syft /tmp/syft && sudo chmod +x /tmp/syft') + c.expect(op_mode_prompt) + + c.sendline('TERM=dumb /tmp/syft --version') + c.expect(op_mode_prompt) + lines = c.before.decode(errors='replace').strip().splitlines() + syft_version = next((l.strip() for l in reversed(lines) if l.strip()), 'unknown') + log.info(f'syft version: {syft_version}') + + # Generate SBOMs using the syft util for the whole filesystem with all layers, output both CycloneDX and SPDX formats + syft_cmd = ( + f'sudo TERM=dumb /tmp/syft / -q' + f' --source-name {iso_name}.iso --source-version {iso_name}' + f' -o cyclonedx-json=/mnt/sbom_transfer/{sbom_file_cdx}' + f' -o spdx-json=/mnt/sbom_transfer/{sbom_file_spdx}' + ) + + log.info(f'Generating SBOMs: {sbom_file_cdx} and {sbom_file_spdx}') + c.sendline(syft_cmd) + c.expect(op_mode_prompt, timeout=1800) + + c.sendline('echo EXITCODE:$\x16?') + i = c.expect(['EXITCODE:0', r'EXITCODE:\d+']) + if i != 0: + raise Exception('syft SBOM generation failed') + + # Unmount transfer disk + c.sendline('sudo umount /mnt/sbom_transfer') + c.expect(op_mode_prompt) + log.info('SBOM files written to transfer disk. The transfer disk was unmounted.') + elif args.sbtest: c.sendline('show secure-boot') c.expect('SecureBoot enabled') @@ -1266,6 +1357,20 @@ try: shutdownVM(c, log, 'Powering off system') c.close() + # Extract SBOM files from transfer disk to host + if args.sbom and transfer_disk_path and args.sbom_output_dir: + log.info('Extracting SBOM files from transfer disk to host.') + mount_point = f'{transfer_disk_path}.mnt' + os.makedirs(mount_point, exist_ok=True) + subprocess.check_output(['mount', '-o', 'loop', transfer_disk_path, mount_point]) + for sbom_f in [sbom_file_cdx, sbom_file_spdx]: + src = os.path.join(mount_point, sbom_f) + if os.path.isfile(src): + shutil.copy2(src, args.sbom_output_dir) + log.info(f'SBOM file saved to: {args.sbom_output_dir}/{sbom_f}') + subprocess.check_output(['umount', mount_point]) + os.rmdir(mount_point) + except EarlyExit: pass @@ -1302,6 +1407,8 @@ if not args.keep: os.remove(diskname_raid) if args.sbtest: os.remove(OVMF_VARS_TMP) + if transfer_disk_path and os.path.isfile(transfer_disk_path): + os.remove(transfer_disk_path) except Exception: log.error('Exception while removing diskimage!') log.error(traceback.format_exc()) -- cgit v1.2.3 From 16ced6d41f030f5621e667abfe0e35aa498bc71b Mon Sep 17 00:00:00 2001 From: Andrii Klymenko Date: Thu, 23 Apr 2026 15:20:09 +0300 Subject: T8542: Add functionality to generate SBOM file from ISO image Exclude the transfer directory from scan. --- scripts/check-qemu-install | 1 + 1 file changed, 1 insertion(+) (limited to 'scripts') diff --git a/scripts/check-qemu-install b/scripts/check-qemu-install index f850a8cb..98b49a64 100755 --- a/scripts/check-qemu-install +++ b/scripts/check-qemu-install @@ -1328,6 +1328,7 @@ try: # Generate SBOMs using the syft util for the whole filesystem with all layers, output both CycloneDX and SPDX formats syft_cmd = ( f'sudo TERM=dumb /tmp/syft / -q' + f' --exclude **/mnt/sbom_transfer/**' f' --source-name {iso_name}.iso --source-version {iso_name}' f' -o cyclonedx-json=/mnt/sbom_transfer/{sbom_file_cdx}' f' -o spdx-json=/mnt/sbom_transfer/{sbom_file_spdx}' -- cgit v1.2.3 From d84d127f91186082df2a786c7e20fe2af55dfa3b Mon Sep 17 00:00:00 2001 From: Andrii Klymenko Date: Thu, 23 Apr 2026 15:59:30 +0300 Subject: T8542: Add functionality to generate SBOM file from ISO image Apply suggestion, add validation that the Syft binary was copied with success. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/check-qemu-install | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/check-qemu-install b/scripts/check-qemu-install index 98b49a64..507b1ea6 100755 --- a/scripts/check-qemu-install +++ b/scripts/check-qemu-install @@ -1315,10 +1315,18 @@ try: raise Exception('Failed to mount SBOM transfer disk inside VM') c.expect(op_mode_prompt) - # Copy the syft binary to the transfer disk and check the version to verify it works before running the SBOM generation process - c.sendline('sudo cp /mnt/sbom_transfer/syft /tmp/syft && sudo chmod +x /tmp/syft') + # Copy the syft binary from the transfer disk and verify it is available before running the SBOM generation process + c.sendline('test -x /mnt/sbom_transfer/syft && echo SYFT_SRC_OK || echo SYFT_SRC_FAIL') + i = c.expect(['SYFT_SRC_OK', 'SYFT_SRC_FAIL']) + if i != 0: + raise Exception('Syft binary is missing or not executable on the SBOM transfer disk') c.expect(op_mode_prompt) + c.sendline('sudo cp /mnt/sbom_transfer/syft /tmp/syft && sudo chmod +x /tmp/syft && test -x /tmp/syft && echo SYFT_COPY_OK || echo SYFT_COPY_FAIL') + i = c.expect(['SYFT_COPY_OK', 'SYFT_COPY_FAIL']) + if i != 0: + raise Exception('Failed to copy Syft binary from SBOM transfer disk to /tmp/syft') + c.expect(op_mode_prompt) c.sendline('TERM=dumb /tmp/syft --version') c.expect(op_mode_prompt) lines = c.before.decode(errors='replace').strip().splitlines() -- cgit v1.2.3 From 3c3df90237c39caa1e5f7e2cf3180fa4f5f4b0bb Mon Sep 17 00:00:00 2001 From: Andrii Klymenko Date: Thu, 23 Apr 2026 16:48:12 +0300 Subject: T8542: Add functionality to generate SBOM file from ISO image Add additional validation step to ensure that the transfer drive is unmounted successfully. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/check-qemu-install | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'scripts') diff --git a/scripts/check-qemu-install b/scripts/check-qemu-install index 507b1ea6..e2a27d69 100755 --- a/scripts/check-qemu-install +++ b/scripts/check-qemu-install @@ -1370,15 +1370,21 @@ try: if args.sbom and transfer_disk_path and args.sbom_output_dir: log.info('Extracting SBOM files from transfer disk to host.') mount_point = f'{transfer_disk_path}.mnt' + mounted = False os.makedirs(mount_point, exist_ok=True) - subprocess.check_output(['mount', '-o', 'loop', transfer_disk_path, mount_point]) - for sbom_f in [sbom_file_cdx, sbom_file_spdx]: - src = os.path.join(mount_point, sbom_f) - if os.path.isfile(src): - shutil.copy2(src, args.sbom_output_dir) - log.info(f'SBOM file saved to: {args.sbom_output_dir}/{sbom_f}') - subprocess.check_output(['umount', mount_point]) - os.rmdir(mount_point) + try: + subprocess.check_output(['mount', '-o', 'loop', transfer_disk_path, mount_point]) + mounted = True + for sbom_f in [sbom_file_cdx, sbom_file_spdx]: + src = os.path.join(mount_point, sbom_f) + if os.path.isfile(src): + shutil.copy2(src, args.sbom_output_dir) + log.info(f'SBOM file saved to: {args.sbom_output_dir}/{sbom_f}') + finally: + if mounted: + subprocess.check_output(['umount', mount_point]) + if os.path.isdir(mount_point): + os.rmdir(mount_point) except EarlyExit: pass -- cgit v1.2.3 From a0f74b5a2565956ce405b7185842cbdd0b4f1768 Mon Sep 17 00:00:00 2001 From: asklymenko Date: Fri, 8 May 2026 14:12:27 +0300 Subject: T8542: Add functionality to generate SBOM file from ISO image. Get Syft from the CDN to prevent a possible supply chain attack --- scripts/check-qemu-install | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/check-qemu-install b/scripts/check-qemu-install index e2a27d69..a04e8c22 100755 --- a/scripts/check-qemu-install +++ b/scripts/check-qemu-install @@ -404,10 +404,22 @@ if args.sbom: subprocess.check_output(['mount', '-o', 'loop', transfer_disk_path, setup_mount]) try: log.info('Downloading syft to transfer disk') - subprocess.check_call( - f'curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b {setup_mount}', - shell=True - ) + if platform.machine() in ['amd64', 'x86_64']: + syft_tar_url = 'https://cdn.vyos.io/tools/syft_1.44.0_linux_amd64.tar.gz' + elif platform.machine() in ['arm64', 'aarch64']: + syft_tar_url = 'https://cdn.vyos.io/tools/syft_1.44.0_linux_arm64.tar.gz' + else: + raise ValueError(f'Unsupported architecture for syft download: {platform.machine()}') + import tarfile, tempfile + with tempfile.TemporaryDirectory() as tar_tmp: + tar_path = os.path.join(tar_tmp, 'syft.tar.gz') + subprocess.check_call(['curl', '-sSfL', '-o', tar_path, syft_tar_url]) + with tarfile.open(tar_path) as tf: + with tf.extractfile(tf.getmember('syft')) as src: + dest_path = os.path.join(setup_mount, 'syft') + with open(dest_path, 'wb') as dst: + dst.write(src.read()) + os.chmod(os.path.join(setup_mount, 'syft'), 0o755) log.info('Syft downloaded to transfer disk') finally: subprocess.check_output(['umount', setup_mount]) -- cgit v1.2.3