Skip to content

Commit 8669f15

Browse files
authored
Merge pull request vyos#1198 from c-po/spdx-refactoring
T8542: refactor SBOM generation - run after ISO build
2 parents c280ccb + 75a495b commit 8669f15

4 files changed

Lines changed: 33 additions & 146 deletions

File tree

Makefile

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,6 @@ qemu-live: checkiso
9090
oci: checkiso
9191
scripts/iso-to-oci $(ISO_PATH)
9292

93-
.PHONY: make_sbom
94-
.ONESHELL:
95-
make_sbom: checkiso
96-
@scripts/check-qemu-install --debug --iso $(ISO_PATH) --sbom --cpu 2 --memory 4 $(if $(SBOM_OUTPUT_DIR),--sbom-output-dir "$(SBOM_OUTPUT_DIR)")
97-
9893
.PHONY: clean
9994
.ONESHELL:
10095
clean:

docker/Dockerfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ RUN apt-get update && apt-get install -y \
348348
debmake \
349349
python3-debian
350350

351+
# Install syft binary required for SBOM generation
352+
RUN cd /tmp && curl -sSfL -o syft.tar.gz \
353+
https://cdn.vyos.io/tools/syft_1.44.0_linux_$(dpkg-architecture -qDEB_HOST_ARCH).tar.gz; \
354+
tar --extract --file=syft.tar.gz syft; mv syft /usr/local/bin/
355+
351356
# Allow password-less 'sudo' for all users in group 'sudo'
352357
RUN sed "s/^%sudo.*/%sudo\tALL=(ALL) NOPASSWD:ALL/g" -i /etc/sudoers && \
353358
echo "vyos_bld\tALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \

scripts/check-qemu-install

Lines changed: 4 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,6 @@ parser.add_argument('--tpmtest', help='Execute TPM encrypted config tests',
125125
action='store_true', default=False)
126126
parser.add_argument('--sbtest', help='Execute Secure Boot tests',
127127
action='store_true', default=False)
128-
parser.add_argument('--sbom', help='Generate SBOM file using syft',
129-
action='store_true', default=False)
130-
parser.add_argument('--sbom-output-dir', help='Host directory to mount and copy SBOM file into',
131-
type=str, default=None)
132128
parser.add_argument('--cloud-init', help='Execute cloud-init tests',
133129
action='store_true', default=False)
134130
parser.add_argument('--qemu-cmd', help='Only generate QEMU launch command',
@@ -149,12 +145,6 @@ args = parser.parse_args()
149145
if os.geteuid() != 0:
150146
exit('You need to have root privileges to run this script.')
151147

152-
if args.sbom and not args.sbom_output_dir:
153-
args.sbom_output_dir = os.getcwd()
154-
155-
if args.sbom_output_dir:
156-
os.makedirs(args.sbom_output_dir, exist_ok=True)
157-
158148
if args.cloud_init:
159149
hostname = CI_CONFIG_ARGS['hostname']
160150
op_mode_prompt = rf'vyos@{hostname}:~\$'
@@ -200,7 +190,7 @@ class EarlyExit(Exception):
200190
def _kvm_exists():
201191
return os.path.exists("/dev/kvm")
202192

203-
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):
193+
def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False, vnc_enabled=False, secure_boot=False):
204194
uefi = ""
205195
uuid = "f48b60b2-e6ad-49ef-9d09-4245d0585e52"
206196
accel = ',accel=kvm' if _kvm_exists() else ''
@@ -286,10 +276,6 @@ def get_qemu_cmd(name, enable_uefi, disk_img, raid=None, iso_img=None, tpm=False
286276
' -tpmdev emulator,id=tpm0,chardev=chrtpm' \
287277
' -device tpm-tis,tpmdev=tpm0'
288278

289-
if transfer_disk:
290-
cmd += f' -drive format=raw,file={transfer_disk},if=none,media=disk,id=drive-transfer,readonly=off' \
291-
f' -device scsi-hd,bus=scsi0.0,drive=drive-transfer,id=transfer-disk'
292-
293279
return cmd
294280

295281
def shutdownVM(c, log, message=''):
@@ -407,45 +393,10 @@ if args.raid:
407393
# must be called after the raid disk as args.disk name is altered in the RAID path
408394
gen_disk(args.disk)
409395

410-
# Create 512MB transfer disk for SBOM generation if requested, download the syft util to use inside the VM for SBOM generation process.
411-
# It will be used to copy generated SBOM files from the VM to the host via the mounted transfer disk.
412-
transfer_disk_path = None
413-
if args.sbom:
414-
_ts = datetime.now().strftime('%Y%m%d-%H%M%S')
415-
_rand = '%04x' % random.randint(0, 0xFFFF)
416-
transfer_disk_path = f'/tmp/vyos-sbom-transfer-{_ts}-{_rand}.img'
417-
subprocess.check_output(['dd', 'if=/dev/zero', f'of={transfer_disk_path}', 'bs=1M', 'count=512'])
418-
subprocess.check_output(['mkfs.vfat', '-F', '32', '-n', 'SBOMXFER', transfer_disk_path])
419-
log.info(f'Created SBOM transfer disk: {transfer_disk_path}')
420-
setup_mount = f'{transfer_disk_path}.setup'
421-
os.makedirs(setup_mount, exist_ok=True)
422-
subprocess.check_output(['mount', '-o', 'loop', transfer_disk_path, setup_mount])
423-
try:
424-
log.info('Downloading syft to transfer disk')
425-
if platform.machine() in ['amd64', 'x86_64']:
426-
syft_tar_url = 'https://cdn.vyos.io/tools/syft_1.44.0_linux_amd64.tar.gz'
427-
elif platform.machine() in ['arm64', 'aarch64']:
428-
syft_tar_url = 'https://cdn.vyos.io/tools/syft_1.44.0_linux_arm64.tar.gz'
429-
else:
430-
raise ValueError(f'Unsupported architecture for syft download: {platform.machine()}')
431-
import tarfile, tempfile
432-
with tempfile.TemporaryDirectory() as tar_tmp:
433-
tar_path = os.path.join(tar_tmp, 'syft.tar.gz')
434-
subprocess.check_call(['curl', '-sSfL', '-o', tar_path, syft_tar_url])
435-
with tarfile.open(tar_path) as tf:
436-
with tf.extractfile(tf.getmember('syft')) as src:
437-
dest_path = os.path.join(setup_mount, 'syft')
438-
with open(dest_path, 'wb') as dst:
439-
dst.write(src.read())
440-
os.chmod(os.path.join(setup_mount, 'syft'), 0o755)
441-
log.info('Syft downloaded to transfer disk')
442-
finally:
443-
subprocess.check_output(['umount', setup_mount])
444-
os.rmdir(setup_mount)
445-
446396
# Create software emulated TPM - clear existing TPM data first - might be a
447397
# leftover from a previous run
448398
clearTPM()
399+
449400
def start_swtpm():
450401
if not os.path.exists(tpm_folder):
451402
os.mkdir(tpm_folder)
@@ -696,7 +647,7 @@ try:
696647
# Installing image to disk
697648
#################################################
698649
log.info('Installing system')
699-
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)
650+
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)
700651
log.debug(f'Executing command: {cmd}')
701652
c = pexpect.spawn(cmd, logfile=stl, timeout=60)
702653

@@ -1297,68 +1248,6 @@ try:
12971248
tmp = 'Configtest failed :/ - check debug output'
12981249
log.error(tmp)
12991250
raise Exception(tmp)
1300-
elif args.sbom:
1301-
# Determine SBOM output file names based on ISO name
1302-
iso_real = os.path.realpath(args.iso) if args.iso else None
1303-
iso_name = os.path.splitext(os.path.basename(iso_real))[0] if iso_real else 'vyos'
1304-
sbom_file_cdx = f'{iso_name}.iso.cdx.json'
1305-
sbom_file_spdx = f'{iso_name}.iso.spdx.json'
1306-
1307-
# Create a mount point for the SBOM transfer disk
1308-
log.info('Mounting transfer disk in VM')
1309-
c.sendline('sudo mkdir -p /mnt/sbom_transfer')
1310-
c.expect(op_mode_prompt)
1311-
1312-
c.sendline('sudo mount $(sudo blkid -L SBOMXFER) /mnt/sbom_transfer')
1313-
c.expect(op_mode_prompt)
1314-
1315-
c.sendline('mountpoint -q /mnt/sbom_transfer && echo MOUNT_OK || echo MOUNT_FAIL')
1316-
i = c.expect(['MOUNT_OK', 'MOUNT_FAIL'])
1317-
if i != 0:
1318-
raise Exception('Failed to mount SBOM transfer disk inside VM')
1319-
c.expect(op_mode_prompt)
1320-
1321-
# Copy the syft binary from the transfer disk and verify it is available before running the SBOM generation process
1322-
c.sendline('test -x /mnt/sbom_transfer/syft && echo SYFT_SRC_OK || echo SYFT_SRC_FAIL')
1323-
i = c.expect(['SYFT_SRC_OK', 'SYFT_SRC_FAIL'])
1324-
if i != 0:
1325-
raise Exception('Syft binary is missing or not executable on the SBOM transfer disk')
1326-
c.expect(op_mode_prompt)
1327-
1328-
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')
1329-
i = c.expect(['SYFT_COPY_OK', 'SYFT_COPY_FAIL'])
1330-
if i != 0:
1331-
raise Exception('Failed to copy Syft binary from SBOM transfer disk to /tmp/syft')
1332-
c.expect(op_mode_prompt)
1333-
c.sendline('TERM=dumb /tmp/syft --version')
1334-
c.expect(op_mode_prompt)
1335-
lines = c.before.decode(errors='replace').strip().splitlines()
1336-
syft_version = next((l.strip() for l in reversed(lines) if l.strip()), 'unknown')
1337-
log.info(f'syft version: {syft_version}')
1338-
1339-
# Generate SBOMs using the syft util for the whole filesystem with all layers, output both CycloneDX and SPDX formats
1340-
syft_cmd = (
1341-
f'sudo TERM=dumb /tmp/syft / -q'
1342-
f' --exclude **/mnt/sbom_transfer/**'
1343-
f' --source-name {iso_name}.iso --source-version {iso_name}'
1344-
f' -o cyclonedx-json=/mnt/sbom_transfer/{sbom_file_cdx}'
1345-
f' -o spdx-json=/mnt/sbom_transfer/{sbom_file_spdx}'
1346-
)
1347-
1348-
log.info(f'Generating SBOMs: {sbom_file_cdx} and {sbom_file_spdx}')
1349-
c.sendline(syft_cmd)
1350-
c.expect(op_mode_prompt, timeout=1800)
1351-
1352-
c.sendline('echo EXITCODE:$\x16?')
1353-
i = c.expect(['EXITCODE:0', r'EXITCODE:\d+'])
1354-
if i != 0:
1355-
raise Exception('syft SBOM generation failed')
1356-
1357-
# Unmount transfer disk
1358-
c.sendline('sudo umount /mnt/sbom_transfer')
1359-
c.expect(op_mode_prompt)
1360-
log.info('SBOM files written to transfer disk. The transfer disk was unmounted.')
1361-
13621251
elif args.sbtest:
13631252
c.sendline('show secure-boot')
13641253
c.expect('SecureBoot enabled')
@@ -1369,26 +1258,6 @@ try:
13691258
shutdownVM(c, log, 'Powering off system')
13701259
c.close()
13711260

1372-
# Extract SBOM files from transfer disk to host
1373-
if args.sbom and transfer_disk_path and args.sbom_output_dir:
1374-
log.info('Extracting SBOM files from transfer disk to host.')
1375-
mount_point = f'{transfer_disk_path}.mnt'
1376-
mounted = False
1377-
os.makedirs(mount_point, exist_ok=True)
1378-
try:
1379-
subprocess.check_output(['mount', '-o', 'loop', transfer_disk_path, mount_point])
1380-
mounted = True
1381-
for sbom_f in [sbom_file_cdx, sbom_file_spdx]:
1382-
src = os.path.join(mount_point, sbom_f)
1383-
if os.path.isfile(src):
1384-
shutil.copy2(src, args.sbom_output_dir)
1385-
log.info(f'SBOM file saved to: {args.sbom_output_dir}/{sbom_f}')
1386-
finally:
1387-
if mounted:
1388-
subprocess.check_output(['umount', mount_point])
1389-
if os.path.isdir(mount_point):
1390-
os.rmdir(mount_point)
1391-
13921261
except EarlyExit:
13931262
pass
13941263

@@ -1425,8 +1294,6 @@ if not args.keep:
14251294
os.remove(diskname_raid)
14261295
if args.sbtest:
14271296
os.remove(OVMF_VARS_TMP)
1428-
if transfer_disk_path and os.path.isfile(transfer_disk_path):
1429-
os.remove(transfer_disk_path)
14301297
except Exception:
14311298
log.error('Exception while removing diskimage!')
14321299
log.error(traceback.format_exc())
@@ -1437,4 +1304,4 @@ if EXCEPTION:
14371304
log.error('The ISO image is not considered usable!')
14381305
sys.exit(1)
14391306

1440-
sys.exit(0)
1307+
sys.exit(0)

scripts/image-build/build-vyos-image

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python3
22
#
3-
# Copyright (C) 2022-2024 VyOS maintainers and contributors
3+
# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
44
#
55
# This program is free software; you can redistribute it and/or modify
66
# it under the terms of the GNU General Public License version 2 or later as
@@ -17,7 +17,6 @@
1717
# File: build-vyos-image
1818
# Purpose: builds VyOS images using a fork of Debian's live-build tool
1919

20-
# Import Python's standard library modules
2120
import re
2221
import os
2322
import sys
@@ -31,11 +30,11 @@ import argparse
3130
import datetime
3231
import functools
3332
import string
33+
import subprocess
3434

3535
class ImageBuildError(Exception):
3636
pass
3737

38-
3938
# argparse converts hyphens to underscores,
4039
# so for lookups in the original options hash we have to convert them back
4140
def field_to_option(s):
@@ -722,11 +721,32 @@ Pin-Priority: 600
722721
cmd("lb build 2>&1")
723722

724723
# Copy the image
725-
shutil.copy("live-image-{0}.hybrid.iso".format(build_config["architecture"]), iso_file)
724+
shutil.copy(f'live-image-{build_config["architecture"]}.hybrid.iso', iso_file)
726725

727726
# Add the image to the manifest
728727
manifest['artifacts'].append(iso_file)
729728

729+
# Now create SBOM
730+
syft_target_dir = 'chroot'
731+
syft_base_path = os.getcwd() + f'/{syft_target_dir}'
732+
cmd = [['syft', syft_target_dir,
733+
'--source-name', 'VyOS', '--source-version', version,
734+
'-o', f'cyclonedx-json=vyos-{version}.cdx.json',
735+
'-o', f'spdx-json=vyos-{version}.spdx.json']]
736+
737+
# syft bug for CycloneDX https://github.com/anchore/syft/issues/4592#issuecomment-4567247328
738+
cmd.append(['sed', '-i', '-e', f's@{syft_base_path}@@g', f'vyos-{version}.cdx.json'])
739+
cmd.append(['sed', '-i', '-e', f's@{syft_base_path}@//@g', f'vyos-{version}.spdx.json'])
740+
741+
for c in cmd:
742+
print(c)
743+
with subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
744+
text=True, bufsize=1) as p:
745+
for line in p.stdout:
746+
sys.stdout.write(line)
747+
sys.stdout.flush()
748+
p.wait()
749+
730750
# If the flavor has `image_format = "iso"`, then the work is done.
731751
# If not, build additional flavors from the ISO.
732752
if build_config["image_format"] != ["iso"]:

0 commit comments

Comments
 (0)