1
1
openwrt/scripts/make-sbom.py
Florian Eckert 10d9dbe838 build: add CycloneDX SBOM processing to apk
Currently, there is no SBOM generation in imagebuilder when the package
system 'apk' is used. This commit adds this feature back. This already
worked for the package system 'opkg'.

Furthermore, generating the SBOM using perl is not reproducible if the
input data has not changed. A different file is always generated. This is
not the case with Python. For this reason, Python is now used to generate
the SBOM for the imagebuilder.

The script has already been prepared so that it can also process the opkg
package system for generating the SBOM.

Signed-off-by: Florian Eckert <fe@dev.tdt.de>
2026-05-17 12:21:09 +02:00

179 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Parse the native package index files into a json file for use by
# downstream tools.
#
"""
import datetime
import email.parser
import json
import uuid
def parse_args():
from argparse import ArgumentParser
parser = ArgumentParser()
# fmt: off
parser.add_argument(dest="source",
help="File name for input, '-' for stdin")
parser.add_argument("-f", "--source-format", required=True,
choices=['apk', 'opkg'],
help=("Required source format of"
" input: 'apk' or 'opkg'"))
parser.add_argument("-m", "--manifest",
help=("File includes the packages to"
" be included in the output"))
# fmt: on
args = parser.parse_args()
return args
def get_apk_sbom(text: str, installed: set) -> list:
packages: dict = json.loads(text)
components: list = []
type_allowed: dict = {
"kernel": "operating-system",
"firmware": "firmware",
"libs": "library"
}
for package in packages["packages"]:
element: dict = {}
# required
if 'name' in package:
name: str = package['name']
element.update({"name": name})
if installed:
if name not in installed:
continue
if 'version' in package:
element.update({"version": package["version"]})
for tag in package.get("tags", []):
if tag.startswith("openwrt:cpe="):
cpe: str = tag.split("=")[-1]
element.update({"cpe": cpe})
# required
type_category: str = ''
for tag in package.get("tags", []):
if tag.startswith("openwrt:section="):
category: str = tag.split("=")[-1]
if type_allowed.get(category):
type_category = type_allowed.get(category)
if type_category:
element.update({"type": type_category})
else:
element.update({"type": "application"})
if 'license' in package:
licenses: list = []
for license in package["license"].split():
licenses.append({"license": {"name": license}})
element.update({"licenses": licenses})
components.append(element)
return components
def get_opkg_sbom(text: str, installed: set) -> list:
components: list = []
type_allowed: dict = {
"kernel": "operating-system",
"firmware": "firmware",
"libs": "library"
}
parser: email.parser.Parser = email.parser.Parser()
chunks: list[str] = text.strip().split("\n\n")
for chunk in chunks:
element: dict = {}
package: dict = parser.parsestr(chunk, headersonly=True)
# required
if 'Package' in package:
name: str = package['Package']
element.update({"name": name})
if installed:
if name not in installed:
continue
if 'Version' in package:
element.update({"version": package['Version']})
if 'CPE-ID' in package:
element.update({"cpe": package['CPE-ID']})
# required
if 'Section' in package:
type_category: str = ''
if type_allowed.get(package['Section']):
type_category = type_allowed.get(package['Section'])
if type_category:
element.update({"type": type_category})
else:
element.update({"type": "application"})
if 'license' in package:
licenses: list = []
for license in package["license"].split():
licenses.append({"license": {"name": license}})
element.update({"licenses": licenses})
if element:
components.append(element)
return components
if __name__ == "__main__":
import sys
args = parse_args()
input = sys.stdin if args.source == "-" else open(args.source, "r")
with input:
text: str = input.read()
# Read manifest file (installed packages)
packages: set = set()
if args.manifest:
with open(args.manifest, 'r') as file:
for line in file:
packages.add(line.split(' - ')[0].strip())
components: list = []
if args.source_format == "apk":
components = get_apk_sbom(text, packages)
elif args.source_format == "opkg":
components = get_opkg_sbom(text, packages)
else:
print("Source format unknown")
raise SystemExit
timestamp: str = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
cyclonedx: dict = {
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:" + str(uuid.uuid4()),
"version": "1",
"metadata": {
"timestamp": timestamp,
},
"components": components,
}
print(json.dumps(cyclonedx, indent=2))