diff --git a/config/Config-build.in b/config/Config-build.in index df2d9101ca..fe16d81d36 100644 --- a/config/Config-build.in +++ b/config/Config-build.in @@ -26,6 +26,14 @@ menu "Global build settings" directory containing machine readable list of built profiles and resulting images. + config JSON_CYCLONEDX_SBOM + bool "Create CycloneDX SBOM JSON" + default BUILDBOT + help + Create a JSON files *.bom.cdx.json in the build + directory containing Software Bill Of Materials in CycloneDX + format. + config ALL_NONSHARED bool "Select all target specific packages by default" select ALL_KMODS diff --git a/include/image.mk b/include/image.mk index fae4d32a8b..3d5d6c1613 100644 --- a/include/image.mk +++ b/include/image.mk @@ -277,6 +277,11 @@ endef define Image/Manifest $(call opkg,$(TARGET_DIR_ORIG)) list-installed > \ $(BIN_DIR)/$(IMG_PREFIX)$(if $(PROFILE_SANITIZED),-$(PROFILE_SANITIZED)).manifest + $(if $(CONFIG_JSON_CYCLONEDX_SBOM), \ + $(SCRIPT_DIR)/package-metadata.pl imgcyclonedxsbom \ + $(TMP_DIR)/.packageinfo \ + $(BIN_DIR)/$(IMG_PREFIX)$(if $(PROFILE_SANITIZED),-$(PROFILE_SANITIZED)).manifest > \ + $(BIN_DIR)/$(IMG_PREFIX)$(if $(PROFILE_SANITIZED),-$(PROFILE_SANITIZED)).bom.cdx.json) endef define Image/gzip-ext4-padded-squashfs diff --git a/package/Makefile b/package/Makefile index 4b8df7f484..8e72d4ec72 100644 --- a/package/Makefile +++ b/package/Makefile @@ -106,6 +106,14 @@ ifdef CONFIG_SIGNED_PACKAGES $(STAGING_DIR_HOST)/bin/usign -S -m Packages -s $(BUILD_KEY); \ ); done endif +ifdef CONFIG_JSON_CYCLONEDX_SBOM + @echo Creating CycloneDX package SBOMs... + @for d in $(PACKAGE_SUBDIRS); do ( \ + [ -d $$d ] && \ + cd $$d || continue; \ + $(SCRIPT_DIR)/package-metadata.pl pkgcyclonedxsbom Packages.manifest > Packages.bom.cdx.json || true; \ + ); done +endif $(curdir)/flags-install:= -j1 diff --git a/scripts/metadata.pm b/scripts/metadata.pm index a00d19f185..587ce7207d 100644 --- a/scripts/metadata.pm +++ b/scripts/metadata.pm @@ -2,7 +2,7 @@ package metadata; use base 'Exporter'; use strict; use warnings; -our @EXPORT = qw(%package %vpackage %srcpackage %category %overrides clear_packages parse_package_metadata parse_target_metadata get_multiline @ignore %usernames %groupnames); +our @EXPORT = qw(%package %vpackage %srcpackage %category %overrides clear_packages parse_package_metadata parse_package_manifest_metadata parse_target_metadata get_multiline @ignore %usernames %groupnames); our %package; our %vpackage; @@ -317,4 +317,42 @@ sub parse_package_metadata($) { return 1; } +sub parse_package_manifest_metadata($) { + my $file = shift; + my $pkg; + my %pkgs; + + open FILE, "<$file" or do { + warn "Cannot open '$file': $!\n"; + return undef; + }; + + while () { + chomp; + /^Package:\s*(.+?)\s*$/ and do { + $pkg = {}; + $pkg->{name} = $1; + $pkg->{depends} = []; + $pkgs{$1} = $pkg; + }; + /^Version:\s*(.+)\s*$/ and $pkg->{version} = $1; + /^Depends:\s*(.+)\s*$/ and $pkg->{depends} = [ split /\s+/, $1 ]; + /^Source:\s*(.+)\s*$/ and $pkg->{source} = $1; + /^SourceName:\s*(.+)\s*$/ and $pkg->{sourcename} = $1; + /^License:\s*(.+)\s*$/ and $pkg->{license} = $1; + /^LicenseFiles:\s*(.+)\s*$/ and $pkg->{licensefiles} = $1; + /^Section:\s*(.+)\s*$/ and $pkg->{section} = $1; + /^SourceDateEpoch: \s*(.+)\s*$/ and $pkg->{sourcedateepoch} = $1; + /^CPE-ID:\s*(.+)\s*$/ and $pkg->{cpe_id} = $1; + /^Architecture:\s*(.+)\s*$/ and $pkg->{architecture} = $1; + /^Installed-Size:\s*(.+)\s*$/ and $pkg->{installedsize} = $1; + /^Filename:\s*(.+)\s*$/ and $pkg->{filename} = $1; + /^Size:\s*(\d+)\s*$/ and $pkg->{size} = $1; + /^SHA256sum:\s*(.*)\s*$/ and $pkg->{sha256sum} = $1; + } + + close FILE; + return %pkgs; +} + 1; diff --git a/scripts/package-metadata.pl b/scripts/package-metadata.pl index dfb2800453..bc61577d22 100755 --- a/scripts/package-metadata.pl +++ b/scripts/package-metadata.pl @@ -4,6 +4,8 @@ use lib "$FindBin::Bin"; use strict; use metadata; use Getopt::Long; +use Time::Piece; +use JSON::PP; my %board; @@ -620,6 +622,173 @@ END_JSON print "[$json]"; } +sub image_manifest_packages($) +{ + my %packages; + my $imgmanifest = shift; + + open FILE, "<$imgmanifest" or return; + while () { + /^(.+?) - (.+)$/ and $packages{$1} = $2; + } + close FILE; + + return %packages; +} + +sub dump_cyclonedxsbom_json { + my (@components) = @_; + + my $uuid = sprintf( + "%04x%04x-%04x-%04x-%04x-%04x%04x%04x", + rand(0xffff), rand(0xffff), rand(0xffff), + rand(0x0fff) | 0x4000, + rand(0x3fff) | 0x8000, + rand(0xffff), rand(0xffff), rand(0xffff) + ); + + my $cyclonedx = { + bomFormat => "CycloneDX", + specVersion => "1.4", + serialNumber => "urn:uuid:$uuid", + version => 1, + metadata => { + timestamp => gmtime->datetime, + }, + "components" => [@components], + }; + + return encode_json($cyclonedx); +} + +sub gen_image_cyclonedxsbom() { + my $pkginfo = shift @ARGV; + my $imgmanifest = shift @ARGV; + my @components; + my %image_packages; + + %image_packages = image_manifest_packages($imgmanifest); + %image_packages or exit 1; + parse_package_metadata($pkginfo) or exit 1; + + $package{"kernel"} = { + license => "GPL-2.0", + cpe_id => "cpe:/o:linux:linux_kernel", + name => "kernel", + }; + + my %abimap; + my @abipkgs = grep { defined $package{$_}->{abi_version} } keys %package; + foreach my $name (@abipkgs) { + my $pkg = $package{$name}; + my $abipkg = $name . $pkg->{abi_version}; + $abimap{$abipkg} = $name; + } + + foreach my $name (sort {uc($a) cmp uc($b)} keys %image_packages) { + my $pkg = $package{$name}; + if (!$pkg) { + $pkg = $package{$abimap{$name}}; + next if !$pkg; + } + + my @licenses; + my @license = split(/\s+/, $pkg->{license}); + foreach my $lic (@license) { + push @licenses, ( + { "license" => { "name" => $lic } } + ); + } + my $type; + if ($pkg->{category}) { + my $category = $pkg->{category}; + my %cat_type = ( + "Firmware" => "firmware", + "Libraries" => "library" + ); + + if ($cat_type{$category}) { + $type = $cat_type{$category}; + } else { + $type = "application"; + } + } + + my $version = $pkg->{version}; + if ($image_packages{$name}) { + $version = $image_packages{$name}; + } + $version =~ s/-\d+$// if $version; + if ($name =~ /^(kernel|kmod-)/ and $version =~ /^(\d+\.\d+\.\d+)/) { + $version = $1; + } + + push @components, { + name => $pkg->{name}, + version => $version, + @licenses > 0 ? (licenses => [ @licenses ]) : (), + $pkg->{cpe_id} ? (cpe => $pkg->{cpe_id}.":".$version) : (), + $type ? (type => $type) : (), + $version ? (version => $version) : (), + }; + } + + print dump_cyclonedxsbom_json(@components); +} + +sub gen_package_cyclonedxsbom() { + my $pkgmanifest = shift @ARGV; + my @components; + my %mpkgs; + + %mpkgs = parse_package_manifest_metadata($pkgmanifest); + %mpkgs or exit 1; + + foreach my $name (sort {uc($a) cmp uc($b)} keys %mpkgs) { + my $pkg = $mpkgs{$name}; + + my @licenses; + my @license = split(/\s+/, $pkg->{license}); + foreach my $lic (@license) { + push @licenses, ( + { "license" => { "name" => $lic } } + ); + } + + my $type; + if ($pkg->{section}) { + my $section = $pkg->{section}; + my %section_type = ( + "firmware" => "firmware", + "libs" => "library" + ); + + if ($section_type{$section}) { + $type = $section_type{$section}; + } else { + $type = "application"; + } + } + + my $version = $pkg->{version}; + $version =~ s/-\d+$// if $version; + if ($name =~ /^(kernel|kmod-)/ and $version =~ /^(\d+\.\d+\.\d+)/) { + $version = $1; + } + + push @components, { + name => $name, + version => $version, + @licenses > 0 ? (licenses => [ @licenses ]) : (), + $pkg->{cpe_id} ? (cpe => $pkg->{cpe_id}.":".$version) : (), + $type ? (type => $type) : (), + $version ? (version => $version) : (), + }; + } + + print dump_cyclonedxsbom_json(@components); +} + sub parse_command() { GetOptions("ignore=s", \@ignore); my $cmd = shift @ARGV; @@ -630,6 +799,8 @@ sub parse_command() { /^source$/ and return gen_package_source(); /^pkgaux$/ and return gen_package_auxiliary(); /^pkgmanifestjson$/ and return gen_package_manifest_json(); + /^imgcyclonedxsbom$/ and return gen_image_cyclonedxsbom(); + /^pkgcyclonedxsbom$/ and return gen_package_cyclonedxsbom(); /^license$/ and return gen_package_license(0); /^licensefull$/ and return gen_package_license(1); /^usergroup$/ and return gen_usergroup_list(); @@ -637,15 +808,17 @@ sub parse_command() { } die < [manifest] Image package manifest in CycloneDX SBOM JSON format + $0 pkgcyclonedxsbom Package manifest in CycloneDX SBOM JSON format + $0 license [file] Package license information $0 licensefull [file] Package license information (full list) - $0 usergroup [file] Package usergroup allocation list + $0 usergroup [file] Package usergroup allocation list $0 version_filter [patchver] [list...] Filter list of version tagged strings Options: