openwrt/scripts/make-sbom.py

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))