@@ -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