@@ -125,6 +125,10 @@ parser.add_argument('--tpmtest', help='Execute TPM encrypted config tests',
125125 action = 'store_true' , default = False )
126126parser .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 )
128132parser .add_argument ('--cloud-init' , help = 'Execute cloud-init tests' ,
129133 action = 'store_true' , default = False )
130134parser .add_argument ('--qemu-cmd' , help = 'Only generate QEMU launch command' ,
@@ -145,6 +149,12 @@ args = parser.parse_args()
145149if 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+
148158if args .cloud_init :
149159 hostname = CI_CONFIG_ARGS ['hostname' ]
150160 op_mode_prompt = rf'vyos@{ hostname } :~\$'
@@ -190,7 +200,7 @@ class EarlyExit(Exception):
190200def _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
281295def 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
394408gen_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
398448clearTPM ()
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+
12601392except 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