build: add CycloneDX SBOM JSON support
authorPetr Štetiar <ynezz@true.cz>
Tue, 24 Oct 2023 08:27:13 +0000 (08:27 +0000)
committerPetr Štetiar <ynezz@true.cz>
Thu, 2 Nov 2023 14:44:47 +0000 (14:44 +0000)
CycloneDX is an open source standard developed by the OWASP foundation.
It supports a wide range of development ecosystems, a comprehensive set
of use cases, and focuses on automation, ease of adoption, and
progressive enhancement of SBOMs (Software Bill Of Materials) throughout
build pipelines.

So lets add support for CycloneDX SBOM for packages and images
manifests.

Signed-off-by: Petr Štetiar <ynezz@true.cz>
(cherry picked from commit d604a07225c5c82b942cd3374cc113ad676a2519)

config/Config-build.in
include/image.mk
package/Makefile
scripts/metadata.pm
scripts/package-metadata.pl

index df2d9101ca99c91fc8cf727f81bd636f67994fa1..fe16d81d36d6223bd88f8df470448cbff3191fb2 100644 (file)
@@ -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
index fae4d32a8bb99dd8bd3d8401754702585ccf74c4..3d5d6c161316d571a6c5d07f8e68072a622d8db8 100644 (file)
@@ -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
index 4b8df7f484de6e63e276c0cecf8aad5cdd96035c..8e72d4ec726dbcfeb6502820bcf29d16813d3551 100644 (file)
@@ -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
 
index a00d19f185a2bc28dea179e935572b19d3e04335..587ce7207d2e6941155782be905b9a5f395358f3 100644 (file)
@@ -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 (<FILE>) {
+               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;
index dfb280045314388c9f26c37915b594e34b3d0c9c..bc61577d2211d9b7b964a32ea0ca78ce02cdae3d 100755 (executable)
@@ -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 (<FILE>) {
+               /^(.+?) - (.+)$/ 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 <<EOF
 Available Commands:
-       $0 mk [file]                            Package metadata in makefile format
-       $0 config [file]                        Package metadata in Kconfig format
+       $0 mk [file]                                    Package metadata in makefile format
+       $0 config [file]                                Package metadata in Kconfig format
        $0 kconfig [file] [config] [patchver]   Kernel config overrides
-       $0 source [file]                        Package source file information
-       $0 pkgaux [file]                        Package auxiliary variables in makefile format
-       $0 pkgmanifestjson [file]               Package manifests in JSON format
-       $0 license [file]                       Package license information
+       $0 source [file]                                Package source file information
+       $0 pkgaux [file]                                Package auxiliary variables in makefile format
+       $0 pkgmanifestjson [file]                       Package manifests in JSON format
+       $0 imgcyclonedxsbom <file> [manifest]   Image package manifest in CycloneDX SBOM JSON format
+       $0 pkgcyclonedxsbom <file>                      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: