summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorasklymenko <a.klymenko@vyos.io>2026-08-12 19:33:47 +0300
committerasklymenko <a.klymenko@vyos.io>2026-08-12 19:33:47 +0300
commit4fb76dcc4580e6bc16fb8fa89b1d0bdcd1dfee26 (patch)
tree7e8eb6fd0193f14a10feaaa074196edc62c10b27
parente14a4895cd5add37240fca9da195832b8a8683e3 (diff)
downloadvyos-build-4fb76dcc4580e6bc16fb8fa89b1d0bdcd1dfee26.tar.gz
vyos-build-4fb76dcc4580e6bc16fb8fa89b1d0bdcd1dfee26.zip
T9203: Enrich SBOM files with additional metadata
-rwxr-xr-xscripts/image-build/build-vyos-image153
1 files changed, 140 insertions, 13 deletions
diff --git a/scripts/image-build/build-vyos-image b/scripts/image-build/build-vyos-image
index 8b1655da..a13e1611 100755
--- a/scripts/image-build/build-vyos-image
+++ b/scripts/image-build/build-vyos-image
@@ -753,24 +753,151 @@ Pin-Priority: 600
# xz streams; syft's own Go-based squashfs/xz decoder apparently only handles
# plain single-filter. Extract squashfs first
print("I: Unpack squashfs for SBOM generation")
- syft_cmd = [['unsquashfs', '-quiet', '-no-progress', '-force', '-dest', syft_target_dir, 'binary/live/filesystem.squashfs']]
- # run syft on extracted content
- syft_cmd.append(['syft', syft_target_dir,
- '--source-name', 'VyOS', '--source-version', version,
- '-o', f'cyclonedx-json={base_filename}.cdx.json',
- '-o', f'spdx-json={base_filename}.spdx.json'])
-
- # syft bug for CycloneDX https://github.com/anchore/syft/issues/4592#issuecomment-4567247328
- syft_cmd.append(['sed', '-i', '-e', f's@{syft_base_path}@@g', f'{base_filename}.cdx.json'])
- syft_cmd.append(['sed', '-i', '-e', f's@{syft_base_path}@//@g', f'{base_filename}.spdx.json'])
-
- for c in syft_cmd:
+ unsquashfs_cmd = ['unsquashfs', '-quiet', '-no-progress', '-force', '-dest', syft_target_dir, 'binary/live/filesystem.squashfs']
+ with subprocess.Popen(unsquashfs_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ text=True, bufsize=1) as p:
+ for line in p.stdout:
+ sys.stdout.write(line)
+ sys.stdout.flush()
+ p.wait()
+
+ # VyOS declares ID=vyos with no ID_LIKE, therefore Syft can't detect the system correctly
+ with open(os.path.join(syft_target_dir, 'etc/os-release'), 'a') as f:
+ f.write('ID_LIKE=debian\n')
+
+ syft_base_flags = ['--base-path', syft_target_dir,
+ '--exclude', './__w/**',
+ '--exclude', '**/external_libs/**',
+ '--source-name', 'VyOS', '--source-version', version]
+
+ # Specify Syft variables to reduce CycloneDX file size
+ cdx_env = os.environ.copy()
+ cdx_env['SYFT_FILE_METADATA_SELECTION'] = 'none'
+ cdx_env['SYFT_RELATIONSHIPS_PACKAGE_FILE_OWNERSHIP'] = 'false'
+
+ # SPDX keeps its defaults (full file cataloguing and ownership relationships).
+ spdx_env = os.environ.copy()
+
+ syft_cmd = [
+ (['syft', 'scan', f'dir:{syft_target_dir}', *syft_base_flags,
+ '-o', f'cyclonedx-json@1.6={base_filename}.cdx.json'], cdx_env),
+ (['syft', 'scan', f'dir:{syft_target_dir}', *syft_base_flags,
+ '-o', f'spdx-json={base_filename}.spdx.json'], spdx_env),
+ # syft bug for CycloneDX https://github.com/anchore/syft/issues/4592#issuecomment-4567247328
+ (['sed', '-i', '-e', f's@{syft_base_path}@@g', f'{base_filename}.cdx.json'], None),
+ (['sed', '-i', '-e', f's@{syft_base_path}@//@g', f'{base_filename}.spdx.json'], None),
+ ]
+
+ for c, e in syft_cmd:
with subprocess.Popen(c, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
- text=True, bufsize=1) as p:
+ text=True, bufsize=1, env=e) as p:
for line in p.stdout:
sys.stdout.write(line)
sys.stdout.flush()
p.wait()
+
+ # Add metadata.authors/supplier/lifecycles information to the SBOM file
+ cdx_file = f'{base_filename}.cdx.json'
+ with open(cdx_file) as f:
+ cdx = json.load(f)
+
+ vyos_author = {'name': 'VyOS maintainers and contributors', 'email': 'maintainers@vyos.io'}
+ vyos_supplier = {'name': 'VyOS Networks', 'url': [build_defaults['website_url']]}
+ cdx['metadata']['authors'] = [vyos_author]
+ cdx['metadata']['supplier'] = vyos_supplier
+
+ cdx['metadata']['lifecycles'] = [{'phase': 'build'}]
+ cdx['metadata']['licenses'] = [{'license': {'id': 'CC0-1.0'}}]
+
+ cdx['metadata']['component']['type'] = 'operating-system'
+ cdx['metadata']['component']['supplier'] = vyos_supplier
+
+ # Add the correct supplier field for Debian packages.
+ publisher_re = re.compile(r'^(?P<name>.*?)\s*[<\[](?P<contact>[^<>\[\]]+)[>\]]$')
+ email_re = re.compile(r'^[^\s@]+@[^\s@]+\.[^\s@]+$')
+ for comp in cdx['components']:
+ publisher = comp.get('publisher')
+ if not publisher:
+ continue
+ m = publisher_re.match(publisher.strip())
+ if m:
+ name, contact = m.group('name').strip(), m.group('contact').strip()
+ if name.lower() == 'none' or contact.lower() == 'none':
+ continue
+ supplier = {}
+ if name:
+ supplier['name'] = name
+ if '@' in contact:
+ supplier['contact'] = [{'email': contact}]
+ elif email_re.match(publisher.strip()):
+ supplier = {'contact': [{'email': publisher.strip()}]}
+ else:
+ supplier = {'name': publisher.strip()}
+ if supplier:
+ comp['supplier'] = supplier
+
+ # VyOS compiles every kernel module itself, so it's correct to list VyOS as the supplier for these modules.
+ for comp in cdx['components']:
+ if comp.get('supplier'):
+ continue
+ found_by = next((p['value'] for p in comp.get('properties', [])
+ if p.get('name') == 'syft:package:foundBy'), None)
+ if found_by == 'linux-kernel-cataloger':
+ comp['supplier'] = vyos_supplier
+
+ # Use the component.author value as the supplier for Python packages.
+ for comp in cdx['components']:
+ if comp.get('supplier'):
+ continue
+ author = comp.get('author')
+ if not author:
+ continue
+ m = publisher_re.match(author.strip())
+ if m:
+ name, contact = m.group('name').strip(), m.group('contact').strip()
+ if name.lower() == 'none' or contact.lower() == 'none':
+ continue
+ supplier = {}
+ if name:
+ supplier['name'] = name
+ if '@' in contact:
+ supplier['contact'] = [{'email': contact}]
+ elif email_re.match(author.strip()):
+ supplier = {'contact': [{'email': author.strip()}]}
+ else:
+ supplier = {'name': author.strip()}
+ if supplier:
+ comp['supplier'] = supplier
+
+ # Specify the supplier for golang.org/x/* modules and the embedded Go stdlib.
+ go_authors_supplier = {'name': 'The Go Authors', 'url': ['https://go.dev']}
+ for comp in cdx['components']:
+ if comp.get('supplier'):
+ continue
+ name = comp.get('name', '')
+ if name.startswith('golang.org/x/') or name == 'stdlib':
+ comp['supplier'] = go_authors_supplier
+
+ with open(cdx_file, 'w') as f:
+ json.dump(cdx, f)
+
+ spdx_file = f'{base_filename}.spdx.json'
+ with open(spdx_file) as f:
+ spdx = json.load(f)
+
+ spdx['creationInfo']['creators'] = [
+ c for c in spdx['creationInfo']['creators'] if not c.startswith('Organization:')
+ ] + ['Organization: VyOS maintainers and contributors (maintainers@vyos.io)']
+
+ spdx['documentNamespace'] = 'https://vyos.io/sbom/' + spdx['documentNamespace'].rsplit('/', 1)[-1]
+ for pkg in spdx['packages']:
+ if pkg.get('SPDXID', '').startswith('SPDXRef-DocumentRoot-'):
+ pkg['supplier'] = 'Organization: VyOS Networks (' + build_defaults['website_url'] + ')'
+ pkg['primaryPackagePurpose'] = 'OPERATING-SYSTEM'
+
+ with open(spdx_file, 'w') as f:
+ json.dump(spdx, f)
+
print("I: Finished SBOM generation")
finally:
# remove temporary unpacked squashfs, even on failure/interruption