mirror of
https://github.com/openwrt/openwrt.git
synced 2026-06-20 03:31:57 +04:00
10d9dbe838
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>
179 lines
5.0 KiB
Python
Executable File
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))
|