Skip to content

Commit 4c224fd

Browse files
authored
Merge pull request #1164 from vyos/T8542-SBOM-current
T8542: Add functionality to generate SBOM file from ISO image
2 parents d98030b + 560bba0 commit 4c224fd

2 files changed

Lines changed: 142 additions & 3 deletions

File tree

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ 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+
9398
.PHONY: clean
9499
.ONESHELL:
95100
clean:

scripts/check-qemu-install

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ 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)
128132
parser.add_argument('--cloud-init', help='Execute cloud-init tests',
129133
action='store_true', default=False)
130134
parser.add_argument('--qemu-cmd', help='Only generate QEMU launch command',
@@ -145,6 +149,12 @@ args = parser.parse_args()
145149
if os.geteuid() != 0:
146150
exit('You need to have root privileges to run this script.')
147151

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+
148158
if args.cloud_init:
149159
hostname = CI_CONFIG_ARGS['hostname']
150160
op_mode_prompt = rf'vyos@{hostname}:~\$'
@@ -190,7 +200,7 @@ class EarlyExit(Exception):
190200
def _kvm_exists():
191201
return os.path.exists("/dev/kvm")
192202

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

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+
279293
return cmd
280294

281295
def shutdownVM(c, log, message=''):
@@ -393,6 +407,42 @@ if args.raid:
393407
# must be called after the raid disk as args.disk name is altered in the RAID path
394408
gen_disk(args.disk)
395409

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+
396446
# Create software emulated TPM - clear existing TPM data first - might be a
397447
# leftover from a previous run
398448
clearTPM()
@@ -646,7 +696,7 @@ try:
646696
# Installing image to disk
647697
#################################################
648698
log.info('Installing system')
649-
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)
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)
650700
log.debug(f'Executing command: {cmd}')
651701
c = pexpect.spawn(cmd, logfile=stl, timeout=60)
652702

@@ -1247,6 +1297,68 @@ try:
12471297
tmp = 'Configtest failed :/ - check debug output'
12481298
log.error(tmp)
12491299
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+
12501362
elif args.sbtest:
12511363
c.sendline('show secure-boot')
12521364
c.expect('SecureBoot enabled')
@@ -1257,6 +1369,26 @@ try:
12571369
shutdownVM(c, log, 'Powering off system')
12581370
c.close()
12591371

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+
12601392
except EarlyExit:
12611393
pass
12621394

@@ -1293,6 +1425,8 @@ if not args.keep:
12931425
os.remove(diskname_raid)
12941426
if args.sbtest:
12951427
os.remove(OVMF_VARS_TMP)
1428+
if transfer_disk_path and os.path.isfile(transfer_disk_path):
1429+
os.remove(transfer_disk_path)
12961430
except Exception:
12971431
log.error('Exception while removing diskimage!')
12981432
log.error(traceback.format_exc())
@@ -1303,4 +1437,4 @@ if EXCEPTION:
13031437
log.error('The ISO image is not considered usable!')
13041438
sys.exit(1)
13051439

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

0 commit comments

Comments
 (0)