#!/usr/bin/env python3 """Reads a CycloneDX JSON file (and optionally the jar it came from). Standard library only. analyse.py inspect SBOM analyse.py compare SBOM JAR analyse.py licences SBOM analyse.py scan SBOM (calls https://api.osv.dev) """ import collections, json, sys, urllib.request, zipfile def load(p): return json.load(open(p)) def purl(c): return c.get("purl") or c["bom-ref"] def inspect(p): d = load(p) m = d["metadata"] print(f"format {d['bomFormat']} {d['specVersion']}, serial {d['serialNumber'][:13]}...") print(f"lifecycle phase: {m['lifecycles'][0]['phase']}") t = m["tools"]["components"][0] print(f"generated by: {t['group']}:{t['name']} {t['version']}") a = m["component"] print(f"describes: {a['type']} {a['group']}:{a['name']}:{a['version']}") c = d["components"] print(f"components: {len(c)}") print("by scope: " + ", ".join(f"{k} {v}" for k, v in sorted(collections.Counter(x.get('scope', '?') for x in c).items()))) print("with a hash: %d of %d" % (sum(1 for x in c if x.get("hashes")), len(c))) print("hash algorithms on the first component: " + ", ".join(h["alg"] for h in c[0]["hashes"])) print("with a licence: %d of %d" % (sum(1 for x in c if x.get("licenses")), len(c))) deps = d["dependencies"] print(f"dependency entries: {len(deps)}") root = [x for x in deps if x["ref"] == a["bom-ref"]][0] print(f"direct dependencies of the application: {len(root['dependsOn'])}") for r in root["dependsOn"]: print(" " + r.split("/", 1)[1].split("?")[0]) print("first three components:") for x in c[:3]: print(" " + purl(x).split("?")[0]) def compare(p, jar): d = load(p) sb = {f"{c['name']}-{c['version']}.jar": c for c in d["components"]} z = zipfile.ZipFile(jar) lib = {n.rsplit("/", 1)[1] for n in z.namelist() if n.startswith("BOOT-INF/lib/") and n.endswith(".jar")} print(f"components in the SBOM: {len(sb)}") print(f"jars in BOOT-INF/lib: {len(lib)}") print(f"in both: {len(set(sb) & lib)}") print("in the SBOM but not in the jar:") for n in sorted(set(sb) - lib): print(" " + n) print("in the jar but not in the SBOM:") for n in sorted(lib - set(sb)): print(" " + n) COPYLEFT = ("GPL", "LGPL", "AGPL", "MPL", "EPL", "CDDL") def licences(p): d = load(p) c = collections.Counter() rows = [] for x in d["components"]: ids = [l["license"].get("id") or l["license"].get("name") for l in x.get("licenses", [])] for i in ids: c[i] += 1 rows.append((x["name"], ids)) print("licence entries (a component with two licences counts once under each):") for k, v in c.most_common(): print(f" {k:36s} {v}") print() naive = [n for n, ids in rows if any(any(k in i for k in ("GPL",)) for i in ids)] print(f"naive rule 'fail on any GPL or LGPL entry' flags {len(naive)}: {', '.join(naive)}") strict = [n for n, ids in rows if ids and all(any(k in i for k in ("GPL",)) for i in ids)] print(f"rule 'fail only when every listed licence is GPL or LGPL' flags {len(strict)}") print("no licence listed: " + (", ".join(n for n, ids in rows if not ids) or "none")) print("dual-licensed components (more than one entry):") for n, ids in rows: if len(ids) > 1: print(f" {n}: {' / '.join(ids)}") def post(url, body): r = urllib.request.Request(url, json.dumps(body).encode(), {"Content-Type": "application/json"}) return json.load(urllib.request.urlopen(r, timeout=30)) def get(url): return json.load(urllib.request.urlopen(url, timeout=30)) def scan(p): d = load(p) comps = d["components"] res = post("https://api.osv.dev/v1/querybatch", {"queries": [{"package": {"purl": purl(x).split("?")[0]}} for x in comps]})["results"] hits = [(x, r["vulns"]) for x, r in zip(comps, res) if r.get("vulns")] print(f"queried {len(comps)} components against api.osv.dev") print(f"components with at least one advisory: {len(hits)}") for x, vs in hits: print(f"{x['group']}:{x['name']}:{x['version']} - {len(vs)} advisories") for v in sorted(vs, key=lambda v: v["id"]): full = get("https://api.osv.dev/v1/vulns/" + v["id"]) cves = [a for a in full.get("aliases", []) if a.startswith("CVE-")] fixed = sorted({e["fixed"] for a in full.get("affected", []) for r in a.get("ranges", []) for e in r.get("events", []) if "fixed" in e}) print(f" {v['id']} {','.join(cves) or '-'} fixed in {','.join(fixed) or '?'}: {full.get('summary', '')[:70]}") if __name__ == "__main__": {"inspect": lambda: inspect(sys.argv[2]), "compare": lambda: compare(sys.argv[2], sys.argv[3]), "licences": lambda: licences(sys.argv[2]), "scan": lambda: scan(sys.argv[2])}[sys.argv[1]]()