--- /dev/null
+#!/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))