diff --git a/tools/vuxml/README.md b/tools/vuxml/README.md new file mode 100644 index 00000000..2939908e --- /dev/null +++ b/tools/vuxml/README.md @@ -0,0 +1,52 @@ +# VuXML advisory converter + +This is relevant to FreeBSD's ports, and possibly any other project using VuXML +in order to track vulnerabilities. + +## Prerequisites + +Clone the following repository: +- https://git.freebsd.org/ports.git + +Install the following packages or modules: +- vuxml +- python-lxml + +## Running the converter + +### Usage + +From VuXML to OSV format: + +``` +Usage: convert_vuxml.py [-e ecosystem][-o output_directory] path/to/vuln.xml +``` + +Where the VuXML vulnerabilities are either provided in a sequence of JSON data +on the standard output, or output to individual files in the output directory. + +From OSV format to VuXML: + +``` +Usage: convert_osv.py [-o output_file] path/to/osv.json... +``` + +Where the OSV files provided are consolidated into a single VuXML file. + +#### Options + +`-e`: +Set a specific ecosystem in the converted output to OSV files (default: +FreeBSD:ports) + +`-o`: +Output directory to place the converted OSV `.json` files (the directory must +exist and have write permissions), or output filename where to write the +converted VuXML file. + +### Example + +``` +$ python3.9 convert_vuxml.py /usr/ports/security/vuxml/vuln.xml +$ python3.9 convert_osv.py 002432c8-ef6a-11ea-ba8f-08002728f74c.json +``` diff --git a/tools/vuxml/convert_osv.py b/tools/vuxml/convert_osv.py new file mode 100644 index 00000000..5e8be679 --- /dev/null +++ b/tools/vuxml/convert_osv.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (C) 1994-2024 The FreeBSD Project. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# Copyright (c) 2024 The FreeBSD Foundation +# +# Portions of this software were developed by Pierre Pronchery +# at Defora Networks GmbH under sponsorship +# from the FreeBSD Foundation. + +"""VuXML to OSV converter.""" +import getopt +import json +from lxml import etree +import sys + +namespace_vuxml = "{http://www.vuxml.org/apps/vuxml-1}" +namespace_xhtml = "{http://www.w3.org/1999/xhtml}" + +url_bid = "https://www.securityfocus.com/bid/" +url_certsa = "https://www.cert.org/advisories/" +url_certvu = "https://www.kb.cert.org/vuls/id/" +url_cve = "https://api.osv.dev/v1/vulns/" +url_freebsd_bugzilla = "https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=" +url_freebsd_sa = "https://www.freebsd.org/security/advisories/FreeBSD-" + + +# convert +def convert(filename, vuxml): + ret = 0 + + try: + with open(filename, "r") as f: + j = json.load(f) + vuln = etree.Element("vuln", vid=j["id"]) + vuxml.append(vuln) + + # topic + topic = etree.Element("topic") + if "summary" in j: + topic.text = j["summary"] + vuln.append(topic) + + # description + if "details" in j: + description = etree.Element("description") + body = etree.Element(namespace_xhtml+"body") + body.text = j["details"] + description.append(body) + vuln.append(description) + + # affects + if "affected" in j: + for affected in j["affected"]: + affects = None + package = None + if "package" in affected \ + and "name" in affected["package"]: + affects = etree.Element("affects") + package = etree.Element("package") + name = etree.Element("name") + name.text = affected["package"]["name"] + package.append(name) + affects.append(package) + if affects is not None \ + and "package" in affected \ + and "name" in affected["package"] \ + and "versions" in affected: + for version in affected["versions"]: + rnge = etree.Element("range") + eq = etree.Element("eq") + eq.text = version + rnge.append(eq) + package.append(rnge) + if affects is not None \ + and "package" in affected \ + and "name" in affected["package"] \ + and "ranges" in affected: + for r in affected["ranges"]: + if "type" in r \ + and r["type"] == "SEMVER" \ + and "events" in r: + rnge = etree.Element("range") + for event in r["events"]: + for k, v in event.items(): + if k == "introduced" and v != "0": + ge = etree.Element("ge") + ge.text = v + rnge.append(ge) + elif k == "fixed": + lt = etree.Element("lt") + lt.text = v + rnge.append(lt) + elif k == "last_affected": + le = etree.Element("le") + le.text = v + rnge.append(le) + if len(rnge) >= 1: + package.append(rnge) + if affects is not None: + vuln.append(affects) + + # references + references = etree.Element("references") + if "references" in j: + for ref in j["references"]: + if ref["type"] == "ADVISORY": + if ref["url"].startswith(url_bid): + r = etree.Element("bid") + url = ref["url"][len(url_bid):] + if url.endswith("/info"): + url = url[:-5] + r.text = url + references.append(r) + elif ref["url"].startswith(url_freebsd_sa): + r = etree.Element("freebsdsa") + url = ref["url"][len(url_freebsd_sa):] + if url.endswith(".asc"): + url = url[:-4] + r.text = url + references.append(r) + elif ref["url"].startswith(url_certsa): + r = etree.Element("certsa") + url = ref["url"][len(url_certsa):] + if url.endswith(".html"): + url = url[:-5] + r.text = url + references.append(r) + elif ref["url"].startswith(url_certvu): + r = etree.Element("certvu") + r.text = ref["url"][len(url_certvu):] + references.append(r) + elif ref["url"].startswith(url_cve): + r = etree.Element("cvename") + r.text = ref["url"][len(url_cve):] + references.append(r) + else: + r = etree.Element("url") + r.text = ref["url"] + references.append(r) + elif ref["type"] == "REPORT": + if ref["url"].startswith(url_freebsd_bugzilla): + r = etree.Element("freebsdpr") + r.text = ref["url"][len(url_freebsd_bugzilla):] + references.append(r) + else: + r = etree.Element("url") + r.text = ref["url"] + references.append(r) + else: + r = etree.Element("url") + r.text = ref["url"] + references.append(r) + if len(references): + vuln.append(references) + + # dates + dates = etree.Element("dates") + entry = j["modified"][0:10] + discovery = entry + modified = None + if "published" in j: + modified = entry + entry = j["published"][0:10] + if "database_specific" in j \ + and "discovery" in j["database_specific"]: + discovery = j["database_specific"]["discovery"][0:10] + date = etree.Element("discovery") + date.text = discovery + dates.append(date) + date = etree.Element("entry") + date.text = entry + dates.append(date) + if modified is not None: + date = etree.Element("modified") + date.text = modified + dates.append(date) + vuln.append(dates) + + # cancelled + if "withdrawn" in dates: + cancelled = etree.Element("cancelled") + vuln.append(cancelled) + except Exception as e: + ret = error(e) + return ret + + +# error +def error(string): + print(f"{sys.argv[0]}: error: {string}", file=sys.stderr) + return 2 + + +# usage +def usage(e=None): + if e is not None: + print(e, file=sys.stderr) + print("Usage: %s [-o output.xml] vuln.json..." + % sys.argv[0], file=sys.stderr) + return 1 + + +# warn +def warn(string): + print(f"{sys.argv[0]}: warning: {string}", file=sys.stderr) + + +# main +def main(): + ret = 0 + + try: + opts, args = getopt.getopt(sys.argv[1:], "o:") + except getopt.GetoptError as e: + return usage(e) + output = None + for name, optarg in opts: + if name == "-o": + output = optarg + else: + return usage("%s: Unsupported option" % name) + + if len(args) < 1: + return usage() + + vuxml = etree.Element(namespace_vuxml+"vuxml") + for arg in args: + if convert(arg, vuxml) != 0: + ret = 2 + break + + if ret == 0: + try: + xml = etree.tostring(vuxml, pretty_print=True) + if output is not None: + with open(output, "w") as f: + print(""" +"""+xml.decode(), file=f) + else: + print(""" +"""+xml.decode()) + except Exception as e: + ret = error(e) + + return ret + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/vuxml/convert_vuxml.py b/tools/vuxml/convert_vuxml.py new file mode 100644 index 00000000..d7cb34dc --- /dev/null +++ b/tools/vuxml/convert_vuxml.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (C) 1994-2024 The FreeBSD Project. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# Copyright (c) 2024 The FreeBSD Foundation +# +# Portions of this software were developed by Pierre Pronchery +# at Defora Networks GmbH under sponsorship +# from the FreeBSD Foundation. +# +# Portions of this software were developed by Tuukka Pasanen +# under sponsorship from the FreeBSD Foundation. + +"""VuXML to OSV converter.""" +import datetime +import getopt +import json +from lxml import etree, html +import os +from pathlib import Path +import re +import sys +import pypandoc + +re_date = re.compile(r"^(19|20)[0-9]{2}-[0-9]{2}-[0-9]{2}$") +re_invalid_package_name = re.compile("[@!#$%^&*()<>?/\\|}{~:]") + +# warn if description has more than X characters +DESCRIPTION_LENGTH = 5000 + +namespace = "{http://www.vuxml.org/apps/vuxml-1}" + +url_advisories = [ + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=", + "https://nvd.nist.gov/vuln/detail/", + "https://github.com/advisories/", + "https://www.debian.org/security/", +] +url_bid = "https://www.securityfocus.com/bid/%s/info" +url_certsa = "https://www.cert.org/advisories/%s.html" +url_certvu = "https://www.kb.cert.org/vuls/id/%s" +url_cve = "https://cveawg.mitre.org/api/cve/%s" +url_freebsd_bugzilla = "https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=%s" +url_freebsd_sa = "https://www.freebsd.org/security/advisories/FreeBSD-%s.asc" +url_reports = [ + "https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=", + "http://bugzilla.mozilla.org/show_bug.cgi?id=", + "https://bugzilla.mozilla.org/show_bug.cgi?id=", + "https://bugzilla.redhat.com/show_bug.cgi?id=", + "https://bugzilla.suse.com/show_bug.cgi?id=", +] + + +class PrefixResolver(etree.Resolver): + def __init__(self, prefix): + self.prefix = prefix + self.result_xml = ( + """\ + + %s-TEST + + """ + % prefix + ) + + def resolve(self, url, pubid, context): + if url.startswith(self.prefix): + print("Resolved url %s as prefix %s" % (url, self.prefix)) + return self.resolve_string(self.result_xml, context) + + +# dateof +def dateof(string): + return datetime.datetime.strptime(string, "%Y-%m-%d") + + +def formatdate(date): + # RFC 3339 ending with Z + return date.strftime("%Y-%m-%dT%H:%M:%SZ") + + +# error +def error(string): + print(f"{sys.argv[0]}: error: {string}", file=sys.stderr) + return 2 + + +# usage +def usage(e=None): + if e is not None: + print(e, file=sys.stderr) + print( + "Usage: %s [-e ecosystem][-o output_directory] vuln.xml" % sys.argv[0], + file=sys.stderr, + ) + return 1 + + +# warn +def warn(string): + print(f"{sys.argv[0]}: warning: {string}", file=sys.stderr) + + +# main +def main(): + try: + opts, args = getopt.getopt(sys.argv[1:], "e:o:n") + except getopt.GetoptError as e: + return usage(e) + ecosystem = "FreeBSD:ports" + output = None + only_new = False + is_kernel = False + + output_id = {} + + for name, optarg in opts: + if name == "-e": + ecosystem = optarg + elif name == "-o": + output = optarg + elif name == "-n": + only_new = True + else: + return usage("%s: Unsupported option" % name) + + if len(args) != 1: + return usage() + + parser = etree.XMLParser(dtd_validation=False) + tree = etree.parse(args[0], parser) + root = tree.getroot() + + ret = 0 + + entries = [] + for vuln in reversed(root): + is_kernel = False + + if vuln.find(namespace + "cancelled") is not None: + continue + + # id + vid = vuln.get("vid") + entry = {"schema_version": "1.7.0"} + dates = {"modified": None, "published": None} + # database_specific + database_specific = {"vid": vid} + + # modified + try: + d = vuln.find(namespace + "dates").find(namespace + "entry").text + if not re_date.match(d): + ret = error("entry date not in YYYY-MM-DD format: {0}".format(d)) + raise + else: + dates_entry = dateof(d) + except Exception as e: + dates_entry = None + try: + d = vuln.find(namespace + "dates").find(namespace + "modified").text + if not re_date.match(d): + ret = error("modified date not in YYYY-MM-DD format: {0}".format(d)) + raise + else: + dates_modified = dateof(d) + except Exception as e: + dates_modified = None + if dates_modified is not None: + entry["modified"] = formatdate(dates_modified) + dates["modified"] = dates_modified + elif dates_entry is not None: + entry["modified"] = formatdate(dates_entry) + dates["modified"] = dates_entry + if dates_entry is not None: + entry["published"] = formatdate(dates_entry) + dates["published"] = dates_entry + + # summary + try: + summary = vuln.find(namespace + "topic").text + except Exception as e: + ret = error(f"{vid} has no topic") + summary = None + if summary is not None: + entry["summary"] = summary + + # details + details = vuln.find(namespace + "description") + if details is None: + ret = error(f"{vid} has no description") + else: + try: + details_html = etree.tostring( + details, encoding="unicode", method="html" + ) + + details = pypandoc.convert_text(details_html, "md", format="html") + + tree = html.fromstring(details_html) + + for elem in tree.iterchildren(): + if elem.tag == "blockquote": + cite = elem.get("cite") + if cite: + if "cite" not in database_specific: + database_specific["cite"] = [] + database_specific["cite"].append(cite) + + if len(details) > DESCRIPTION_LENGTH: + warn("%s: description truncated (> %s)" % (vid, DESCRIPTION_LENGTH)) + details = details[0:DESCRIPTION_LENGTH] + except Exception as e: + ret = error( + "%s could not parse description: %s: %s" + % (vid, type(e).__name__, e) + ) + details = None + if details is not None: + entry["details"] = details + + # references + references = [] + + if "cite" in database_specific: + for cite in database_specific["cite"]: + references.append({"type": "REPORT", "url": cite}) + + refs = vuln.find(namespace + "references") + for ref in refs: + is_appendable = False + if ref.text is None or len(ref.text) == 0 or type(ref) is etree._Comment: + continue + + cur_tag = ref.tag.removeprefix(namespace) + + if ref.tag == namespace + "bid": + reference = {"type": "ADVISORY", "url": url_bid % ref.text} + is_appendable = True + elif ref.tag == namespace + "certsa": + reference = {"type": "ADVISORY", "url": url_certsa % ref.text} + is_appendable = True + elif ref.tag == namespace + "certvu": + reference = {"type": "ADVISORY", "url": url_certvu % ref.text} + is_appendable = True + elif ref.tag == namespace + "cvename": + reference = {"type": "ADVISORY", "url": url_cve % ref.text} + is_appendable = True + elif ref.tag == namespace + "freebsdpr" and len(ref.text.split("/")) == 2: + id = ref.text.split("/")[1] + reference = {"type": "REPORT", "url": url_freebsd_bugzilla % id} + is_appendable = True + elif ref.tag == namespace + "freebsdsa": + reference = {"type": "ADVISORY", "url": url_freebsd_sa % ref.text} + is_appendable = True + elif ref.tag == namespace + "mlist": + reference = {"type": "DISCUSSION", "url": ref.text} + elif ref.tag == namespace + "url": + if ( + "cite" in database_specific + and ref.text in database_specific["cite"] + ): + continue + + # As there can be also URL for this then do not add + # double entries + if ( + "references" in database_specific + and "cvename" in database_specific["references"] + ): + is_cvename = False + for cvename in database_specific["references"]["cvename"]: + if cvename in ref.text and "mitre.org" in ref.text: + is_cvename = True + break + if is_cvename: + continue + + reference = {"type": "WEB", "url": ref.text} + for prefix in url_advisories: + if str(ref.text).startswith(prefix): + reference["type"] = "ADVISORY" + break + if reference["type"] == "WEB": + for prefix in url_reports: + if str(ref.text).startswith(prefix): + reference["type"] = "REPORT" + break + else: + continue + + if is_appendable: + if "references" not in database_specific: + database_specific["references"] = {} + if cur_tag not in database_specific["references"]: + database_specific["references"][cur_tag] = [] + database_specific["references"][cur_tag].append(ref.text) + + references.append(reference) + if len(references) > 0: + entry["references"] = references + + # affected + affected = [] + affects = vuln.find(namespace + "affects") + for package in affects.findall(namespace + "package"): + + # affected: package + for name in package.findall(namespace + "name"): + a = {} + if re_invalid_package_name.search(name.text) is not None: + ret = error("%s package with invalid name: %s" % (vid, name.text)) + continue + cur_ecosystem = ecosystem + if name.text == "FreeBSD-kernel": + cur_ecosystem = "FreeBSD:kernel" + is_kernel = True + + p = {"ecosystem": cur_ecosystem, "name": name.text} + a["package"] = p + + key_order = ["introduced", "fixed", "last_affected", "limit"] + # affected: ranges + try: + ranges = [] + versions = [] + for e in package.findall(namespace + "range"): + events = [] + semver = {"type": "ECOSYSTEM"} + + # affected: ranges + event = {} + ge = e.find(namespace + "ge") + if ge is not None and len(ge.text) > 0: + if ge.text != "*": + event["introduced"] = ge.text + else: + event["introduced"] = "0" + gt = e.find(namespace + "gt") + if gt is not None and len(gt.text) > 0: + if gt.text != "*": + # Not correct. Should be fixed + event["introduced"] = gt.text + ",1" + else: + event["introduced"] = "0" + le = e.find(namespace + "le") + if le is not None and len(le.text) > 0: + event["fixed"] = le.text + if le.text != "*": + event["fixed"] = le.text + else: + event["fixed"] = "0" + lt = e.find(namespace + "lt") + if lt is not None and len(lt.text) > 0: + if lt.text != "*": + event["fixed"] = lt.text + else: + event["fixed"] = "0" + if "fixed" in event or "introduced" in event: + if "introduced" not in event: + event["introduced"] = "0" + + # Always introduced and fixed after that + # just for the sanity + for order_key in key_order: + if order_key in event: + events.append({order_key: event[order_key]}) + + eq = e.find(namespace + "eq") + if eq is not None and len(eq.text) > 0 and eq.text != "*": + events.append({"introduced": eq.text}) + events.append({"fixed": eq.text}) + + if len(events) > 0: + semver["events"] = events + ranges.append(semver) + except Exception as e: + warn(e, file=sys.stderr) + ranges = [] + if len(ranges) > 0: + a["ranges"] = ranges + if len(versions) > 0: + a["versions"] = versions + + if len(a) > 0: + affected.append(a) + if len(affected) > 0: + entry["affected"] = affected + + try: + d = vuln.find(namespace + "dates").find(namespace + "discovery").text + if not re_date.match(d): + ret = error("discovery date not in YYYY-MM-DD format: {0}".format(d)) + raise + else: + dates_discovery = dateof(d) + except Exception as e: + dates_discovery = None + if dates_discovery is not None: + database_specific["discovery"] = formatdate(dates_discovery) + if len(database_specific) > 0: + entry["database_specific"] = database_specific + + if output is not None: + try: + date_str = None + date_obj = None + year_str = None + if dates["published"] is not None: + date_str = dates["published"].strftime("%Y-%m-%d") + year_str = dates["published"].strftime("%Y") + date_obj = dates["published"] + elif dates["modified"] is not None: + date_str = dates["modified"].strftime("%Y-%m-%d") + year_str = dates["published"].strftime("%Y") + date_obj = dates["modified"] + + if date_str is None: + raise Exception(f"There is no date available") + + file_base_name = "FreeBSD" + + # File name can be with date of release: + # FreeBSD-20250101.json + # or just running number + # FreeBSD-2025-0001.json + # When using running id then there won't be yearly + # subdirs + if year_str not in output_id: + output_id[year_str] = 0 + output_id[year_str] += 1 + output_file = f"{file_base_name}-{year_str}-{output_id[year_str]:04}" + + # Make sure that is same as filename + entry["id"] = output_file + + output_year_path = output + "/" + year_str + + if os.path.isdir(output_year_path) is False: + os.mkdir(output_year_path) + year_date = datetime.date(int(year_str), 1, 1) + year_time_ts = int(year_date.strftime("%s")) + os.utime(output_year_path, (year_time_ts, year_time_ts)) + + affected_array = entry["affected"] + + # If output is not flat then output path will be like + # 2025/somepackage/ with flat only 2025/ + # output_path_with_name = output_year_path + "/" + output_name + output_path_with_name = output_year_path + + if os.path.isdir(output_path_with_name) is False: + os.mkdir(output_path_with_name) + + output_with_suffix = output_file + ".json" + + output_full_path = output_path_with_name + "/" + output_with_suffix + + if os.path.isfile(output_full_path) is True: + if only_new: + continue + print("OSVf file already created: " + output_full_path) + + # This one have to open file with binary to write + # as bytes + with open(output_full_path, "w") as f: + print(json.dumps(entry, indent=4, sort_keys=True), file=f) + + if os.path.isfile(output_full_path): + timeint = int(date_obj.strftime("%s")) + os.utime(output_full_path, (timeint, timeint)) + + except Exception as e: + print("There was an error: ", e) + ret = error(e) + else: + entries.append(entry) + + if output is None: + if len(entries) == 1: + print(json.dumps(entries[0], indent=4)) + else: + print(json.dumps(entries, indent=4)) + + return ret + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/vuxml/testdata/OSV/UBUNTU-CVE-2025-3454.json b/tools/vuxml/testdata/OSV/UBUNTU-CVE-2025-3454.json new file mode 100644 index 00000000..ac7a0ffd --- /dev/null +++ b/tools/vuxml/testdata/OSV/UBUNTU-CVE-2025-3454.json @@ -0,0 +1,46 @@ +{ + "schema_version": "1.6.3", + "id": "UBUNTU-CVE-2025-3454", + "details": "[Unknown description]", + "aliases": [], + "related": [ + "CVE-2025-3454" + ], + "published": "2025-04-28T00:00:00Z", + "modified": "2025-04-29T16:36:00Z", + "affected": [ + { + "package": { + "ecosystem": "Ubuntu:Pro:16.04:LTS", + "name": "grafana", + "purl": "pkg:deb/ubuntu/grafana@2.6.0+dfsg-1?arch=source&distro=esm-apps/xenial" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + } + ] + } + ], + "versions": [ + "2.6.0+dfsg-1" + ], + "ecosystem_specific": { + "ubuntu_priority": "medium" + } + } + ], + "references": [ + { + "type": "REPORT", + "url": "https://ubuntu.com/security/CVE-2025-3454" + }, + { + "type": "REPORT", + "url": "https://www.cve.org/CVERecord?id=CVE-2025-3454" + } + ] +} \ No newline at end of file diff --git a/tools/vuxml/testdata/OSV/UBUNTU-CVE-2025-3454.xml b/tools/vuxml/testdata/OSV/UBUNTU-CVE-2025-3454.xml new file mode 100644 index 00000000..55b53565 --- /dev/null +++ b/tools/vuxml/testdata/OSV/UBUNTU-CVE-2025-3454.xml @@ -0,0 +1,27 @@ + + + + + + [Unknown description] + + + + grafana + + 2.6.0+dfsg-1 + + + + + https://ubuntu.com/security/CVE-2025-3454 + https://www.cve.org/CVERecord?id=CVE-2025-3454 + + + 2025-04-29 + 2025-04-28 + 2025-04-29 + + + + diff --git a/tools/vuxml/testdata/VUXML/CVE-2025-29087.json b/tools/vuxml/testdata/VUXML/CVE-2025-29087.json new file mode 100644 index 00000000..59151daf --- /dev/null +++ b/tools/vuxml/testdata/VUXML/CVE-2025-29087.json @@ -0,0 +1,54 @@ +{ + "schema_version": "1.7.0", + "modified": "2025-04-30T00:00:00Z", + "published": "2025-04-30T00:00:00Z", + "summary": "sqlite -- integer overflow", + "details": "cve@mitre.org reports:\n\n> In SQLite 3.44.0 through 3.49.0 before 3.49.1, the concat_ws() SQL\n> function can cause memory to be written beyond the end of a\n> malloc-allocated buffer. If the separator argument is\n> attacker-controlled and has a large string (e.g., 2MB or more), an\n> integer overflow occurs in calculating the size of the result buffer,\n> and thus malloc may not allocate enough memory.\n", + "references": [ + { + "type": "REPORT", + "url": "https://gist.github.com/ylwango613/a44a29f1ef074fa783e29f04a0afd62a" + }, + { + "type": "ADVISORY", + "url": "https://cveawg.mitre.org/api/cve/CVE-2025-29087" + }, + { + "type": "ADVISORY", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-29087" + } + ], + "affected": [ + { + "package": { + "ecosystem": "FreeBSD:ports", + "name": "sqlite" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "3.49.1" + } + ] + } + ] + } + ], + "database_specific": { + "vid": "409206f6-25e6-11f0-9360-b42e991fc52e", + "cite": [ + "https://gist.github.com/ylwango613/a44a29f1ef074fa783e29f04a0afd62a" + ], + "references": { + "cvename": [ + "CVE-2025-29087" + ] + }, + "discovery": "2025-04-07T00:00:00Z" + } +} diff --git a/tools/vuxml/testdata/VUXML/CVE-2025-29087.xml b/tools/vuxml/testdata/VUXML/CVE-2025-29087.xml new file mode 100644 index 00000000..74a1aabd --- /dev/null +++ b/tools/vuxml/testdata/VUXML/CVE-2025-29087.xml @@ -0,0 +1,37 @@ + + + + + sqlite -- integer overflow + + + sqlite + 3.49.1 + + + + +

cve@mitre.org reports:

+
+

+ In SQLite 3.44.0 through 3.49.0 before 3.49.1, the + concat_ws() SQL function can cause memory to be written + beyond the end of a malloc-allocated buffer. If the + separator argument is attacker-controlled and has a large + string (e.g., 2MB or more), an integer overflow occurs in + calculating the size of the result buffer, and thus malloc + may not allocate enough memory. +

+
+ +
+ + CVE-2025-29087 + https://nvd.nist.gov/vuln/detail/CVE-2025-29087 + + + 2025-04-07 + 2025-04-30 + +
+
diff --git a/tools/vuxml/testdata/VUXML/CVE-2025-3454.json b/tools/vuxml/testdata/VUXML/CVE-2025-3454.json new file mode 100644 index 00000000..2b53c8f7 --- /dev/null +++ b/tools/vuxml/testdata/VUXML/CVE-2025-3454.json @@ -0,0 +1,137 @@ +{ + "schema_version": "1.7.0", + "modified": "2025-04-24T00:00:00Z", + "published": "2025-04-24T00:00:00Z", + "summary": "Grafana -- Authorization bypass in data source proxy API", + "details": "Grafana Labs reports:\n\n> This vulnerability, which was discovered while reviewing a pull\n> request from an external contributor, effects Grafana's data source\n> proxy API and allows authorization checks to be bypassed by adding an\n> extra slash character (/) in the URL path. Among Grafana-maintained\n> data sources, the vulnerability only affects the read paths of\n> Prometheus (all flavors) and Alertmanager when configured with basic\n> authorization.\n>\n> The CVSS score for this vulnerability is [5.0\n> MEDIUM](https://www.first.org/cvss/calculator/3-1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N).\n", + "references": [ + { + "type": "REPORT", + "url": "https://grafana.com/blog/2025/04/22/grafana-security-release-medium-and-high-severity-fixes-for-cve-2025-3260-cve-2025-2703-cve-2025-3454/" + }, + { + "type": "ADVISORY", + "url": "https://cveawg.mitre.org/api/cve/CVE-2025-3454" + } + ], + "affected": [ + { + "package": { + "ecosystem": "FreeBSD:ports", + "name": "grafana" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "8.0.0" + }, + { + "fixed": "10.4.17+security-01" + } + ] + }, + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "11.0.0" + }, + { + "fixed": "11.2.8+security-01" + } + ] + }, + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "11.3.0" + }, + { + "fixed": "11.3.5+security-01" + } + ] + }, + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "11.4.0" + }, + { + "fixed": "11.4.3+security-01" + } + ] + }, + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "11.5.0" + }, + { + "fixed": "11.5.3+security-01" + } + ] + }, + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "11.6.0" + }, + { + "fixed": "11.6.0+security-01" + } + ] + } + ] + }, + { + "package": { + "ecosystem": "FreeBSD:ports", + "name": "grafana8" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "8.0.0" + } + ] + } + ] + }, + { + "package": { + "ecosystem": "FreeBSD:ports", + "name": "grafana9" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "9.0.0" + } + ] + } + ] + } + ], + "database_specific": { + "vid": "310f5923-211c-11f0-8ca6-6c3be5272acd", + "cite": [ + "https://grafana.com/blog/2025/04/22/grafana-security-release-medium-and-high-severity-fixes-for-cve-2025-3260-cve-2025-2703-cve-2025-3454/" + ], + "references": { + "cvename": [ + "CVE-2025-3454" + ] + }, + "discovery": "2025-03-25T00:00:00Z" + } +} diff --git a/tools/vuxml/testdata/VUXML/CVE-2025-3454.xml b/tools/vuxml/testdata/VUXML/CVE-2025-3454.xml new file mode 100644 index 00000000..629acf7b --- /dev/null +++ b/tools/vuxml/testdata/VUXML/CVE-2025-3454.xml @@ -0,0 +1,50 @@ + + + + + Grafana -- Authorization bypass in data source proxy API + + + grafana + 8.0.010.4.17+security-01 + 11.0.011.2.8+security-01 + 11.3.011.3.5+security-01 + 11.4.011.4.3+security-01 + 11.5.011.5.3+security-01 + 11.6.011.6.0+security-01 + + + grafana8 + 8.0.0 + + + grafana9 + 9.0.0 + + + + +

Grafana Labs reports:

+
+

This vulnerability, which was discovered while reviewing a pull + request from an external contributor, effects Grafana’s data source + proxy API and allows authorization checks to be bypassed by adding + an extra slash character (/) in the URL path. Among Grafana-maintained + data sources, the vulnerability only affects the read paths + of Prometheus (all flavors) and Alertmanager when configured with + basic authorization.

+

The CVSS score for this vulnerability is + 5.0 MEDIUM.

+
+ +
+ + CVE-2025-3454 + https://grafana.com/blog/2025/04/22/grafana-security-release-medium-and-high-severity-fixes-for-cve-2025-3260-cve-2025-2703-cve-2025-3454/ + + + 2025-03-25 + 2025-04-24 + +
+
diff --git a/tools/vuxml/testdata/VUXML/CVE-2025-43859.json b/tools/vuxml/testdata/VUXML/CVE-2025-43859.json new file mode 100644 index 00000000..3fd1271e --- /dev/null +++ b/tools/vuxml/testdata/VUXML/CVE-2025-43859.json @@ -0,0 +1,111 @@ +{ + "schema_version": "1.7.0", + "modified": "2025-04-29T00:00:00Z", + "published": "2025-04-29T00:00:00Z", + "summary": "h11 accepts some malformed Chunked-Encoding bodies", + "details": "h11 reports:\n\n> h11 is a Python implementation of HTTP/1.1. Prior to version 0.16.0, a\n> leniency in h11\\'s parsing of line t erminators in chunked-coding\n> message bodies can lead to request smuggling vulnerabilities under\n> certain conditions. This issu e has been patched in version 0.16.0.\n> Since exploitation requires the combination of buggy h11 with a buggy\n> (reverse) proxy, fixing either component is sufficient to mitigate\n> this issue.\n", + "references": [ + { + "type": "REPORT", + "url": "https://github.com/python-hyper/h11/security/advisories/GHSA-vqfr-h8mv-ghfj" + }, + { + "type": "ADVISORY", + "url": "https://cveawg.mitre.org/api/cve/CVE-2025-43859" + }, + { + "type": "ADVISORY", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-43859" + } + ], + "affected": [ + { + "package": { + "ecosystem": "FreeBSD:ports", + "name": "py39-h11" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.16.0" + } + ] + } + ] + }, + { + "package": { + "ecosystem": "FreeBSD:ports", + "name": "py310-h11" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.16.0" + } + ] + } + ] + }, + { + "package": { + "ecosystem": "FreeBSD:ports", + "name": "py311-h11" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.16.0" + } + ] + } + ] + }, + { + "package": { + "ecosystem": "FreeBSD:ports", + "name": "py312-h11" + }, + "ranges": [ + { + "type": "ECOSYSTEM", + "events": [ + { + "introduced": "0" + }, + { + "fixed": "0.16.0" + } + ] + } + ] + } + ], + "database_specific": { + "vid": "df126e23-24fa-11f0-ab92-f02f7497ecda", + "cite": [ + "https://github.com/python-hyper/h11/security/advisories/GHSA-vqfr-h8mv-ghfj" + ], + "references": { + "cvename": [ + "CVE-2025-43859" + ] + }, + "discovery": "2025-04-24T00:00:00Z" + } +} diff --git a/tools/vuxml/testdata/VUXML/CVE-2025-43859.xml b/tools/vuxml/testdata/VUXML/CVE-2025-43859.xml new file mode 100644 index 00000000..7615a41c --- /dev/null +++ b/tools/vuxml/testdata/VUXML/CVE-2025-43859.xml @@ -0,0 +1,32 @@ + + + + + h11 accepts some malformed Chunked-Encoding bodies + + + py39-h11 + py310-h11 + py311-h11 + py312-h11 + 0.16.0 + + + + +

h11 reports:

+
+

h11 is a Python implementation of HTTP/1.1. Prior to version 0.16.0, a leniency in h11's parsing of line t erminators in chunked-coding message bodies can lead to request smuggling vulnerabilities under certain conditions. This issu e has been patched in version 0.16.0. Since exploitation requires the combination of buggy h11 with a buggy (reverse) proxy, fixing either component is sufficient to mitigate this issue.

+
+ +
+ + CVE-2025-43859 + https://nvd.nist.gov/vuln/detail/CVE-2025-43859 + + + 2025-04-24 + 2025-04-29 + +
+