diff --git a/config/Config-build.in b/config/Config-build.in
index df2d9101ca99c91fc8cf727f81bd636f67994fa1..fe16d81d36d6223bd88f8df470448cbff3191fb2 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 fae4d32a8bb99dd8bd3d8401754702585ccf74c4..3d5d6c161316d571a6c5d07f8e68072a622d8db8 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 4b8df7f484de6e63e276c0cecf8aad5cdd96035c..8e72d4ec726dbcfeb6502820bcf29d16813d3551 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 a00d19f185a2bc28dea179e935572b19d3e04335..587ce7207d2e6941155782be905b9a5f395358f3 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 (<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;
diff --git a/scripts/package-metadata.pl b/scripts/package-metadata.pl
index dfb280045314388c9f26c37915b594e34b3d0c9c..bc61577d2211d9b7b964a32ea0ca78ce02cdae3d 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 (<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: