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