Skip to content

Commit 4fb76dc

Browse files
committed
T9203: Enrich SBOM files with additional metadata
1 parent e14a489 commit 4fb76dc

1 file changed

Lines changed: 140 additions & 13 deletions

File tree

scripts/image-build/build-vyos-image

Lines changed: 140 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -753,24 +753,151 @@ Pin-Priority: 600
753753
# xz streams; syft's own Go-based squashfs/xz decoder apparently only handles
754754
# plain single-filter. Extract squashfs first
755755
print("I: Unpack squashfs for SBOM generation")
756-
syft_cmd = [['unsquashfs', '-quiet', '-no-progress', '-force', '-dest', syft_target_dir, 'binary/live/filesystem.squashfs']]
757-
# run syft on extracted content
758-
syft_cmd.append(['syft', syft_target_dir,
759-
'--source-name', 'VyOS', '--source-version', version,
760-
'-o', f'cyclonedx-json={base_filename}.cdx.json',
761-
'-o', f'spdx-json={base_filename}.spdx.json'])
762-
763-
# syft bug for CycloneDX https://github.com/anchore/syft/issues/4592#issuecomment-4567247328
764-
syft_cmd.append(['sed', '-i', '-e', f's@{syft_base_path}@@g', f'{base_filename}.cdx.json'])
765-
syft_cmd.append(['sed', '-i', '-e', f's@{syft_base_path}@//@g', f'{base_filename}.spdx.json'])
766-
767-
for c in syft_cmd:
756+
unsquashfs_cmd = ['unsquashfs', '-quiet', '-no-progress', '-force', '-dest', syft_target_dir, 'binary/live/filesystem.squashfs']
757+
with subprocess.Popen(unsquashfs_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
758+
text=True, bufsize=1) as p:
759+
for line in p.stdout:
760+
sys.stdout.write(line)
761+
sys.stdout.flush()
762+
p.wait()
763+
764+
# VyOS declares ID=vyos with no ID_LIKE, therefore Syft can't detect the system correctly
765+
with open(os.path.join(syft_target_dir, 'etc/os-release'), 'a') as f:
766+
f.write('ID_LIKE=debian\n')
767+
768+
syft_base_flags = ['--base-path', syft_target_dir,
769+
'--exclude', './__w/**',
770+
'--exclude', '**/external_libs/**',
771+
'--source-name', 'VyOS', '--source-version', version]
772+
773+
# Specify Syft variables to reduce CycloneDX file size
774+
cdx_env = os.environ.copy()
775+
cdx_env['SYFT_FILE_METADATA_SELECTION'] = 'none'
776+
cdx_env['SYFT_RELATIONSHIPS_PACKAGE_FILE_OWNERSHIP'] = 'false'
777+
778+
# SPDX keeps its defaults (full file cataloguing and ownership relationships).
779+
spdx_env = os.environ.copy()
780+
781+
syft_cmd = [
782+
(['syft', 'scan', f'dir:{syft_target_dir}', *syft_base_flags,
783+
'-o', f'cyclonedx-json@1.6={base_filename}.cdx.json'], cdx_env),
784+
(['syft', 'scan', f'dir:{syft_target_dir}', *syft_base_flags,
785+
'-o', f'spdx-json={base_filename}.spdx.json'], spdx_env),
786+
# syft bug for CycloneDX https://github.com/anchore/syft/issues/4592#issuecomment-4567247328
787+
(['sed', '-i', '-e', f's@{syft_base_path}@@g', f'{base_filename}.cdx.json'], None),
788+
(['sed', '-i', '-e', f's@{syft_base_path}@//@g', f'{base_filename}.spdx.json'], None),
789+
]
790+
791+
for c, e in syft_cmd:
768792
with subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
769-
text=True, bufsize=1) as p:
793+
text=True, bufsize=1, env=e) as p:
770794
for line in p.stdout:
771795
sys.stdout.write(line)
772796
sys.stdout.flush()
773797
p.wait()
798+
799+
# Add metadata.authors/supplier/lifecycles information to the SBOM file
800+
cdx_file = f'{base_filename}.cdx.json'
801+
with open(cdx_file) as f:
802+
cdx = json.load(f)
803+
804+
vyos_author = {'name': 'VyOS maintainers and contributors', 'email': 'maintainers@vyos.io'}
805+
vyos_supplier = {'name': 'VyOS Networks', 'url': [build_defaults['website_url']]}
806+
cdx['metadata']['authors'] = [vyos_author]
807+
cdx['metadata']['supplier'] = vyos_supplier
808+
809+
cdx['metadata']['lifecycles'] = [{'phase': 'build'}]
810+
cdx['metadata']['licenses'] = [{'license': {'id': 'CC0-1.0'}}]
811+
812+
cdx['metadata']['component']['type'] = 'operating-system'
813+
cdx['metadata']['component']['supplier'] = vyos_supplier
814+
815+
# Add the correct supplier field for Debian packages.
816+
publisher_re = re.compile(r'^(?P<name>.*?)\s*[<\[](?P<contact>[^<>\[\]]+)[>\]]$')
817+
email_re = re.compile(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')
818+
for comp in cdx['components']:
819+
publisher = comp.get('publisher')
820+
if not publisher:
821+
continue
822+
m = publisher_re.match(publisher.strip())
823+
if m:
824+
name, contact = m.group('name').strip(), m.group('contact').strip()
825+
if name.lower() == 'none' or contact.lower() == 'none':
826+
continue
827+
supplier = {}
828+
if name:
829+
supplier['name'] = name
830+
if '@' in contact:
831+
supplier['contact'] = [{'email': contact}]
832+
elif email_re.match(publisher.strip()):
833+
supplier = {'contact': [{'email': publisher.strip()}]}
834+
else:
835+
supplier = {'name': publisher.strip()}
836+
if supplier:
837+
comp['supplier'] = supplier
838+
839+
# VyOS compiles every kernel module itself, so it's correct to list VyOS as the supplier for these modules.
840+
for comp in cdx['components']:
841+
if comp.get('supplier'):
842+
continue
843+
found_by = next((p['value'] for p in comp.get('properties', [])
844+
if p.get('name') == 'syft:package:foundBy'), None)
845+
if found_by == 'linux-kernel-cataloger':
846+
comp['supplier'] = vyos_supplier
847+
848+
# Use the component.author value as the supplier for Python packages.
849+
for comp in cdx['components']:
850+
if comp.get('supplier'):
851+
continue
852+
author = comp.get('author')
853+
if not author:
854+
continue
855+
m = publisher_re.match(author.strip())
856+
if m:
857+
name, contact = m.group('name').strip(), m.group('contact').strip()
858+
if name.lower() == 'none' or contact.lower() == 'none':
859+
continue
860+
supplier = {}
861+
if name:
862+
supplier['name'] = name
863+
if '@' in contact:
864+
supplier['contact'] = [{'email': contact}]
865+
elif email_re.match(author.strip()):
866+
supplier = {'contact': [{'email': author.strip()}]}
867+
else:
868+
supplier = {'name': author.strip()}
869+
if supplier:
870+
comp['supplier'] = supplier
871+
872+
# Specify the supplier for golang.org/x/* modules and the embedded Go stdlib.
873+
go_authors_supplier = {'name': 'The Go Authors', 'url': ['https://go.dev']}
874+
for comp in cdx['components']:
875+
if comp.get('supplier'):
876+
continue
877+
name = comp.get('name', '')
878+
if name.startswith('golang.org/x/') or name == 'stdlib':
879+
comp['supplier'] = go_authors_supplier
880+
881+
with open(cdx_file, 'w') as f:
882+
json.dump(cdx, f)
883+
884+
spdx_file = f'{base_filename}.spdx.json'
885+
with open(spdx_file) as f:
886+
spdx = json.load(f)
887+
888+
spdx['creationInfo']['creators'] = [
889+
c for c in spdx['creationInfo']['creators'] if not c.startswith('Organization:')
890+
] + ['Organization: VyOS maintainers and contributors (maintainers@vyos.io)']
891+
892+
spdx['documentNamespace'] = 'https://vyos.io/sbom/' + spdx['documentNamespace'].rsplit('/', 1)[-1]
893+
for pkg in spdx['packages']:
894+
if pkg.get('SPDXID', '').startswith('SPDXRef-DocumentRoot-'):
895+
pkg['supplier'] = 'Organization: VyOS Networks (' + build_defaults['website_url'] + ')'
896+
pkg['primaryPackagePurpose'] = 'OPERATING-SYSTEM'
897+
898+
with open(spdx_file, 'w') as f:
899+
json.dump(spdx, f)
900+
774901
print("I: Finished SBOM generation")
775902
finally:
776903
# remove temporary unpacked squashfs, even on failure/interruption

0 commit comments

Comments
 (0)