Skip to content

Commit fbf08af

Browse files
authored
Merge pull request #1266 from asklymenko/rolling
T9203: Enrich SBOM files with additional metadata
2 parents 6b7e4d8 + b9c957d commit fbf08af

1 file changed

Lines changed: 147 additions & 14 deletions

File tree

scripts/image-build/build-vyos-image

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

0 commit comments

Comments
 (0)