From dfff7f9fbbd37b688c860adcbdf5f4003051ed89 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 15 Mar 2023 19:41:24 +0530 Subject: [PATCH 01/10] Skip unsupported PURLs in Deps Signed-off-by: Keshav Priyadarshi --- vulntotal/datasources/deps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vulntotal/datasources/deps.py b/vulntotal/datasources/deps.py index 413c0d18b..3188b9302 100644 --- a/vulntotal/datasources/deps.py +++ b/vulntotal/datasources/deps.py @@ -42,6 +42,8 @@ def datasource_advisory(self, purl) -> Iterable[VendorData]: A list of VendorData objects containing the advisory information. """ payload = generate_meta_payload(purl) + if not payload: + return response = self.fetch_json_response(payload) if response: advisories = parse_advisories_from_meta(response) From 9cd44adab2f6a9fc5ce8fc1165a6acbf9207b66f Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 15 Mar 2023 19:59:36 +0530 Subject: [PATCH 02/10] Report unmodified affected versions Signed-off-by: Keshav Priyadarshi --- vulntotal/datasources/github.py | 4 ++-- .../tests/test_data/github/parse_advisory-expected.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vulntotal/datasources/github.py b/vulntotal/datasources/github.py index 6192d6240..154a31fe6 100644 --- a/vulntotal/datasources/github.py +++ b/vulntotal/datasources/github.py @@ -101,8 +101,8 @@ def parse_advisory(interesting_edges, purl) -> Iterable[VendorData]: """ for edge in interesting_edges: node = edge["node"] - aliases = [alias["value"] for alias in get_item(node, "advisory", "identifiers")] - affected_versions = node["vulnerableVersionRange"].strip().replace(" ", "").split(",") + aliases = [aliase["value"] for aliase in get_item(node, "advisory", "identifiers")] + affected_versions = [node["vulnerableVersionRange"].strip()] parsed_fixed_versions = get_item(node, "firstPatchedVersion", "identifier") fixed_versions = [parsed_fixed_versions] if parsed_fixed_versions else [] yield VendorData( diff --git a/vulntotal/tests/test_data/github/parse_advisory-expected.json b/vulntotal/tests/test_data/github/parse_advisory-expected.json index 60979f260..1594603cf 100644 --- a/vulntotal/tests/test_data/github/parse_advisory-expected.json +++ b/vulntotal/tests/test_data/github/parse_advisory-expected.json @@ -2,7 +2,7 @@ { "purl": "pkg:generic/namespace/test", "affected_versions": [ - "<2.7.2" + "< 2.7.2" ], "fixed_versions": [ "2.7.2" @@ -15,7 +15,7 @@ { "purl": "pkg:generic/namespace/test", "affected_versions": [ - "<2.11.3" + "< 2.11.3" ], "fixed_versions": [ "2.11.3" @@ -28,7 +28,7 @@ { "purl": "pkg:generic/namespace/test", "affected_versions": [ - "<2.8.1" + "< 2.8.1" ], "fixed_versions": [ "2.8.1" @@ -41,7 +41,7 @@ { "purl": "pkg:generic/namespace/test", "affected_versions": [ - "<2.10.1" + "< 2.10.1" ], "fixed_versions": [ "2.10.1" From 8728e05a17339b13c138a58b4775edfdc292fc97 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 15 Mar 2023 20:21:35 +0530 Subject: [PATCH 03/10] Fix function name in recursive path resolver Signed-off-by: Keshav Priyadarshi --- vulntotal/datasources/gitlab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulntotal/datasources/gitlab.py b/vulntotal/datasources/gitlab.py index ae805ab00..27b0e7517 100644 --- a/vulntotal/datasources/gitlab.py +++ b/vulntotal/datasources/gitlab.py @@ -180,7 +180,7 @@ def get_casesensitive_slug(path, package_slug): # If the namespace/subfolder contains multiple packages, then progressive transverse through folders tree if package_slug.lower().startswith(slug_flatpath.lower()): - return get_gitlab_style_slug(slug_flatpath, package_slug) + return get_casesensitive_slug(slug_flatpath, package_slug) payload[0]["variables"]["nextPageCursor"] = paginated_tree["pageInfo"]["endCursor"] has_next = paginated_tree["pageInfo"]["hasNextPage"] From 7ad42b76120fbcb92822ab4d58177ff04620a671 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Wed, 15 Mar 2023 21:36:41 +0530 Subject: [PATCH 04/10] Handle Snyk advisory with missing CVE Signed-off-by: Keshav Priyadarshi --- vulntotal/datasources/snyk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vulntotal/datasources/snyk.py b/vulntotal/datasources/snyk.py index 5b2418071..11734af6d 100644 --- a/vulntotal/datasources/snyk.py +++ b/vulntotal/datasources/snyk.py @@ -273,7 +273,8 @@ def parse_html_advisory(advisory_html, snyk_id, affected, purl) -> VendorData: cve_span = advisory_soup.find("span", class_="cve") if cve_span: cve_anchor = cve_span.find("a", class_="vue--anchor") - aliases.append(cve_anchor["id"]) + if cve_anchor: + aliases.append(cve_anchor.get("id")) how_to_fix = advisory_soup.find( "div", class_="vue--block vuln-page__instruction-block vue--block--instruction" From 57add778b45705e5ba0889e352510462691777f5 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Thu, 16 Mar 2023 17:25:57 +0530 Subject: [PATCH 05/10] Support advisory comparison across different DataSources - Add debug flag --vers to display equivalent normalized versions for corresponding native ranges. - Add debug flag --no-compare to run the CLI without comparison. - Auto-adjust text table width based on the terminal width. Signed-off-by: Keshav Priyadarshi --- vulntotal/vulntotal_cli.py | 345 +++++++++++++++++++++++++++++++++---- 1 file changed, 308 insertions(+), 37 deletions(-) diff --git a/vulntotal/vulntotal_cli.py b/vulntotal/vulntotal_cli.py index c65007ba8..f820e7b2c 100755 --- a/vulntotal/vulntotal_cli.py +++ b/vulntotal/vulntotal_cli.py @@ -11,6 +11,8 @@ import concurrent.futures import json +import math +import os import pydoc import click @@ -19,9 +21,12 @@ import yaml from packageurl import PackageURL from texttable import Texttable +from univers.normalized_range import NormalizedVersionRanges +from vulnerabilities.package_managers import VERSION_API_CLASSES_BY_PACKAGE_TYPE from vulntotal.datasources import DATASOURCE_REGISTRY from vulntotal.validator import VendorData +from vulntotal.vulntotal_utils import get_item @click.command() @@ -42,8 +47,6 @@ metavar="FILE", help="Write output as YAML to FILE. Use '-' to print on screen.", ) - -# hidden debug options @click.option( "-l", "--list", @@ -53,6 +56,8 @@ required=False, help="List available datasources.", ) + +# hidden debug options @click.option( "-e", "--enable", @@ -88,7 +93,7 @@ hidden=True, multiple=False, required=False, - help="Report the raw responses from each datasource. Used for debugging. Used for debugging.", + help="Report the raw responses from each datasource. Used for debugging.", ) @click.option( "--no-threading", @@ -118,6 +123,24 @@ required=False, help="Do not group output by vulnerability/CVE. Used for debugging.", ) +@click.option( + "--vers", + "vers", + is_flag=True, + hidden=True, + multiple=False, + required=False, + help="Show normalized vers. Used for debugging.", +) +@click.option( + "--no-compare", + "no_compare", + is_flag=True, + hidden=True, + multiple=False, + required=False, + help="Do not compare datasource output. Used for debugging.", +) @click.help_option("-h", "--help") def handler( purl, @@ -131,6 +154,8 @@ def handler( json_output, yaml_output, no_group, + vers, + no_compare, ): """ Search all the available vulnerabilities databases for the package-url PURL. @@ -155,16 +180,16 @@ def handler( get_raw_response(purl, active_datasource) elif json_output: - write_json_output(purl, active_datasource, json_output, no_threading) + write_json_output(purl, active_datasource, json_output, no_threading, no_group, no_compare) elif yaml_output: - write_yaml_output(purl, active_datasource, yaml_output, no_threading) + write_yaml_output(purl, active_datasource, yaml_output, no_threading, no_group, no_compare) elif no_group: prettyprint(purl, active_datasource, pagination, no_threading) elif purl: - prettyprint_group_by_cve(purl, active_datasource, pagination, no_threading) + prettyprint_group_by_cve(purl, active_datasource, pagination, no_threading, vers, no_compare) def get_valid_datasources(datasources): @@ -209,6 +234,9 @@ def list_supported_ecosystem(datasources): def formatted_row(datasource, advisory): + if not advisory: + return [datasource.upper(), "", "", ""] + aliases = "\n".join(advisory.aliases) affected = " ".join(advisory.affected_versions) fixed = " ".join(advisory.fixed_versions) @@ -253,16 +281,24 @@ def run_datasources(purl, datasources, no_threading=False): return vulnerabilities -class VendorDataEncoder(json.JSONEncoder): +class VulntotalEncoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, VendorData): + if isinstance(obj, VendorData) or isinstance(obj, NormalizedVersionRanges): return obj.to_dict() return json.JSONEncoder.default(self, obj) -def write_json_output(purl, datasources, json_output, no_threading): +def write_json_output(purl, datasources, json_output, no_threading, no_group, no_compare): + results = {"purl": purl, "datasources": list(datasources.keys())} + vulnerabilities = run_datasources(purl, datasources, no_threading) - return json.dump(vulnerabilities, json_output, cls=VendorDataEncoder, indent=2) + if no_group: + results.update(vulnerabilities) + else: + grouped_by_cve = group_by_cve(vulnerabilities, PackageURL.from_string(purl), no_compare) + results.update(grouped_by_cve) + + return json.dump(results, json_output, cls=VulntotalEncoder, indent=2) def noop(self, *args, **kw): @@ -272,9 +308,38 @@ def noop(self, *args, **kw): yaml.emitter.Emitter.process_tag = noop -def write_yaml_output(purl, datasources, yaml_output, no_threading): +def write_yaml_output(purl, datasources, yaml_output, no_threading, no_group, no_compare): + results = {"purl": purl, "datasources": list(datasources.keys())} + vulnerabilities = run_datasources(purl, datasources, no_threading) - return yaml.dump(vulnerabilities, yaml_output, default_flow_style=False, indent=2) + if no_group: + results.update(vulnerabilities) + else: + grouped_by_cve = group_by_cve(vulnerabilities, PackageURL.from_string(purl), no_compare) + serialize_normalized_range(grouped_by_cve, no_compare) + results.update(grouped_by_cve) + + return yaml.dump(results, yaml_output, default_flow_style=False, indent=2, sort_keys=False) + + +def serialize_normalized_range(grouped_by_cve, no_compare): + if no_compare: + return + for cve, value in grouped_by_cve.items(): + if cve in ("NOCVE", "NOADVISORY"): + continue + for datasource, resources in value.items(): + for resource in resources: + affected_versions = resource.get("normalized_affected_versions") + fixed_versions = resource.get("normalized_fixed_versions") + if isinstance(affected_versions, NormalizedVersionRanges): + resource["normalized_affected_versions"] = [ + str(vers) for vers in affected_versions.version_ranges + ] + if isinstance(fixed_versions, NormalizedVersionRanges): + resource["normalized_fixed_versions"] = [ + str(vers) for vers in fixed_versions.version_ranges + ] def prettyprint(purl, datasources, pagination, no_threading): @@ -285,11 +350,7 @@ def prettyprint(purl, datasources, pagination, no_threading): active_datasources = ", ".join(sorted([x.upper() for x in datasources.keys()])) metadata = f"PURL: {purl}\nActive datasources: {active_datasources}\n\n" - table = Texttable() - table.set_cols_dtype(["t", "t", "t", "t"]) - table.set_cols_align(["c", "l", "l", "l"]) - table.set_cols_valign(["t", "t", "a", "t"]) - table.header(["DATASOURCE", "ALIASES", "AFFECTED", "FIXED"]) + table = get_texttable(no_group=True) for datasource, advisories in vulnerabilities.items(): if not advisories: @@ -302,47 +363,255 @@ def prettyprint(purl, datasources, pagination, no_threading): pydoc.pager(metadata + table.draw()) if pagination else click.echo(metadata + table.draw()) -def group_by_cve(vulnerabilities): +NORMALIZED_VERSION_RANGE_BY_DATASOURCE = { + "deps": NormalizedVersionRanges.from_discrete, + "github": NormalizedVersionRanges.from_github, + "gitlab": NormalizedVersionRanges.from_gitlab, + "oss_index": None, + "osv": NormalizedVersionRanges.from_discrete, + "snyk": NormalizedVersionRanges.from_snyk, + "vulnerablecode": NormalizedVersionRanges.from_discrete, +} + + +def group_by_cve(vulnerabilities, purl, no_compare): grouped_by_cve = {} - no_cve = [] - no_advisory = [] + nocve = {} + noadvisory = {} for datasource, advisories in vulnerabilities.items(): if not advisories: - no_advisory.append([datasource.upper(), "", "", ""]) - + if datasource not in noadvisory: + noadvisory[datasource] = [] + noadvisory[datasource].append( + { + "advisory": None, + } + ) for advisory in advisories: cve = next((x for x in advisory.aliases if x.startswith("CVE")), None) if not cve: - no_cve.append(formatted_row(datasource, advisory)) + if datasource not in nocve: + nocve[datasource] = [] + nocve[datasource].append( + { + "advisory": advisory, + } + ) continue if cve not in grouped_by_cve: - grouped_by_cve[cve] = [] - grouped_by_cve[cve].append(formatted_row(datasource, advisory)) - grouped_by_cve["NOCVE"] = no_cve - grouped_by_cve["NOADVISORY"] = no_advisory + grouped_by_cve[cve] = {} + + if datasource not in grouped_by_cve[cve]: + grouped_by_cve[cve][datasource] = [] + grouped_by_cve[cve][datasource].append( + { + "advisory": advisory, + } + ) + grouped_by_cve["NOCVE"] = nocve + grouped_by_cve["NOADVISORY"] = noadvisory + if not no_compare: + normalize_version_ranges(grouped_by_cve, purl) + compare(grouped_by_cve) return grouped_by_cve -def prettyprint_group_by_cve(purl, datasources, pagination, no_threading): +def normalize_version_ranges(grouped_by_cve, purl): + package_versions = get_all_versions(purl) + + for cve, value in grouped_by_cve.items(): + if cve in ("NOCVE", "NOADVISORY"): + continue + for datasource, resources in value.items(): + for resource in resources: + advisory = resource["advisory"] + normalized_affected_versions = [] + normalized_fixed_versions = [] + datasource_normalizer = NORMALIZED_VERSION_RANGE_BY_DATASOURCE.get(datasource) + if datasource_normalizer and advisory.affected_versions: + try: + normalized_affected_versions = datasource_normalizer( + advisory.affected_versions, purl.type, package_versions + ) + except Exception as err: + normalized_affected_versions = [err] + + if advisory.fixed_versions: + try: + normalized_fixed_versions = NormalizedVersionRanges.from_discrete( + advisory.fixed_versions, purl.type, package_versions + ) + except Exception as err: + normalized_fixed_versions = [err] + + resource["normalized_affected_versions"] = normalized_affected_versions + resource["normalized_fixed_versions"] = normalized_fixed_versions + + +def compare(grouped_by_cve): + for cve, value in grouped_by_cve.items(): + if cve in ("NOCVE", "NOADVISORY"): + continue + sources = list(value.keys()) + board = {source: {} for source in sources} + """ + A typical board after comparison may look like this. + + board = { + "github":{ + "snyk": 0, + "gitlab": 1, + "deps": 0, + "vulnerablecode": 1, + "osv": 1, + "oss_index": 1, + }, + "snyk":{ + "github": 0, + "gitlab": 1, + "deps": 0, + "vulnerablecode": 1, + "osv": 1, + "oss_index": 1, + }, + ... + } + """ + for datasource, resources in value.items(): + normalized_affected_versions_a = get_item(resources, 0, "normalized_affected_versions") + normalized_fixed_versions_a = get_item(resources, 0, "normalized_fixed_versions") + if normalized_fixed_versions_a and normalized_affected_versions_a: + for source in sources: + if ( + source == datasource + or source in board[datasource] + or datasource in board[source] + ): + continue + normalized_affected_versions_b = get_item( + value, source, 0, "normalized_affected_versions" + ) + normalized_fixed_versions_b = get_item( + value, source, 0, "normalized_fixed_versions" + ) + board[datasource][source] = 0 + board[source][datasource] = 0 + if ( + normalized_fixed_versions_a == normalized_fixed_versions_b + and normalized_affected_versions_a == normalized_affected_versions_b + ): + board[datasource][source] = 1 + board[source][datasource] = 1 + + maximum = max([sum(list(table.values())) for table in board.values()]) + datasource_count = len(sources) + for datasource, table in board.items(): + if maximum == 0: + # NA if only one advisory else TC aka `Total Collision`. + value[datasource][0]["score"] = "TC" if datasource_count > 1 else "NA" + continue + value[datasource][0]["score"] = (sum(list(table.values())) / maximum) * 100 + + +def prettyprint_group_by_cve(purl, datasources, pagination, no_threading, vers, no_compare): vulnerabilities = run_datasources(purl, datasources, no_threading) if not vulnerabilities: return - grouped_by_cve = group_by_cve(vulnerabilities) + grouped_by_cve = group_by_cve(vulnerabilities, PackageURL.from_string(purl), no_compare) active_datasource = ", ".join(sorted([x.upper() for x in datasources.keys()])) metadata = f"PURL: {purl}\nActive DataSources: {active_datasource}\n\n" + table = get_texttable(no_compare=no_compare) + + for cve, value in grouped_by_cve.items(): + for datasource, resources in value.items(): + row = [cve] + formatted_row(datasource, resources[0].get("advisory")) + if not no_compare: + row.append(resources[0].get("score", "NA")) + + table.add_row(row) + + if not no_compare and vers and "score" in resources[0]: + na_affected = get_item(resources, 0, "normalized_affected_versions") + na_fixed = get_item(resources, 0, "normalized_fixed_versions") + na_affected = ( + na_affected.version_ranges + if isinstance(na_affected, NormalizedVersionRanges) + else na_affected + ) + na_fixed = ( + na_fixed.version_ranges + if isinstance(na_fixed, NormalizedVersionRanges) + else na_fixed + ) + na_affected = "\n".join([str(i) for i in na_affected]) + na_fixed = "\n".join([str(i) for i in na_fixed]) + table.add_row(["", "", "", na_affected, na_fixed, ""]) + + pydoc.pager(metadata + table.draw()) if pagination else click.echo(metadata + table.draw()) + + +def strip_leading_v(version): + if version.startswith("v"): + return version[1:] + return version + + +def get_texttable(no_group=False, no_compare=False): + quantum = 100 / 125 + terminal_width = os.get_terminal_size().columns + line_factor = terminal_width / 100 + + column_5x = math.floor(5 * quantum * line_factor) + column_15x = math.floor(15 * quantum * line_factor) + column_20x = math.floor(20 * quantum * line_factor) + table = Texttable() - table.set_cols_dtype(["a", "a", "a", "a", "a"]) - table.set_cols_align(["l", "l", "l", "l", "l"]) - table.set_cols_valign(["t", "t", "t", "a", "t"]) - table.header(["CVE", "DATASOURCE", "ALIASES", "AFFECTED", "FIXED"]) - for cve, advisories in grouped_by_cve.items(): - for count, advisory in enumerate(advisories): - table.add_row([cve] + advisory) + if no_group: + table.set_cols_dtype(["t", "t", "t", "t"]) + table.set_cols_align(["c", "l", "l", "l"]) + table.set_cols_valign(["t", "t", "a", "t"]) + table.set_cols_width([column_20x, column_20x, column_20x, column_20x]) + table.header(["DATASOURCE", "ALIASES", "AFFECTED", "FIXED"]) + return table + + if no_compare: + table.set_cols_dtype(["a", "a", "a", "a", "a"]) + table.set_cols_align(["l", "l", "l", "l", "l"]) + table.set_cols_valign(["t", "t", "t", "a", "t"]) + table.set_cols_width([column_20x, column_15x, column_20x, column_20x, column_20x]) + table.header(["CVE", "DATASOURCE", "ALIASES", "AFFECTED", "FIXED"]) + return table + + table.set_cols_dtype(["a", "a", "a", "a", "a", "a"]) + table.set_cols_align(["l", "l", "l", "l", "l", "l"]) + table.set_cols_valign(["t", "t", "t", "a", "t", "t"]) + table.set_cols_width([column_20x, column_15x, column_20x, column_20x, column_20x, column_5x]) + table.header(["CVE", "DATASOURCE", "ALIASES", "AFFECTED", "FIXED", "SCORE"]) + + return table + + +def get_all_versions(purl: PackageURL): + if purl.type not in VERSION_API_CLASSES_BY_PACKAGE_TYPE: + return - pydoc.pager(metadata + table.draw()) if pagination else click.echo(metadata + table.draw()) + versionAPI = None + package_name = None + + if purl.type == "maven": + package_name = f"{purl.namespace}:{purl.name}" + if purl.type in ("composer", "golang", "github"): + package_name = f"{purl.namespace}/{purl.name}" + if purl.type in ("nuget", "pypi", "gem", "npm", "hex", "deb", "cargo"): + package_name = purl.name + + versionAPI = VERSION_API_CLASSES_BY_PACKAGE_TYPE.get(purl.type)() + all_versions = versionAPI.fetch(package_name) + + return [strip_leading_v(package_version.value) for package_version in all_versions] if __name__ == "__main__": @@ -366,5 +635,7 @@ def prettyprint_group_by_cve(purl, datasources, pagination, no_threading): --no-threading Run DataSources sequentially. -p, --pagination Enable default pagination. --no-group Don't group by CVE. + --vers Show normalized vers. + --no-compare Do not compare datasource output. -h, --help Show this message and exit. """ From 31830fd37a99b5ebceaeedc0066f7d66caa4546d Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 26 Jul 2024 13:55:24 +0530 Subject: [PATCH 06/10] Bump univers to v30.12.0 Signed-off-by: Keshav Priyadarshi --- requirements.txt | 2 +- setup.cfg | 2 +- .../apache-kafka-improver-expected.json | 202 ++++++++++++++++++ 3 files changed, 204 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5ebc73fc5..f73700e83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -106,7 +106,7 @@ toml==0.10.2 tomli==2.0.1 traitlets==5.1.1 typing_extensions==4.1.1 -univers==30.11.0 +univers==30.12.0 urllib3==1.26.19 wcwidth==0.2.5 websocket-client==0.59.0 diff --git a/setup.cfg b/setup.cfg index a030f0ded..cb1168e26 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,7 +71,7 @@ install_requires = #essentials packageurl-python>=0.10.5rc1 - univers>=30.11.0 + univers>=30.12.0 license-expression>=21.6.14 # file and data formats diff --git a/vulnerabilities/tests/test_data/apache_kafka/apache-kafka-improver-expected.json b/vulnerabilities/tests/test_data/apache_kafka/apache-kafka-improver-expected.json index f70b17721..7548361c2 100644 --- a/vulnerabilities/tests/test_data/apache_kafka/apache-kafka-improver-expected.json +++ b/vulnerabilities/tests/test_data/apache_kafka/apache-kafka-improver-expected.json @@ -1,4 +1,206 @@ [ + { + "vulnerability_id": null, + "aliases": [ + "CVE-2021-38153" + ], + "confidence": 100, + "summary": "Some components in Apache Kafka use Arrays.equals to validate a password or key, which is vulnerable to timing attacks that make brute force attacks for such credentials more likely to be successful. Users should upgrade to 2.8.1 or higher, or 3.0.0 or higher where this vulnerability has been fixed.", + "affected_purls": [ + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.0", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.1", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.2", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.3", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.4", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.5", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.6", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.7", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.8", + "qualifiers": "", + "subpath": "" + } + ], + "fixed_purl": null, + "references": [ + { + "reference_id": "CVE-2021-38153", + "url": "https://kafka.apache.org/cve-list", + "severities": [] + }, + { + "reference_id": "CVE-2021-38153", + "url": "https://kafka.apache.org/cve-list#CVE-2021-38153", + "severities": [] + }, + { + "reference_id": "CVE-2021-38153", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-38153", + "severities": [] + } + ], + "weaknesses": [] + }, + { + "vulnerability_id": null, + "aliases": [ + "CVE-2019-12399" + ], + "confidence": 100, + "summary": "When Connect workers in Apache Kafka 2.0.0, 2.0.1, 2.1.0, 2.1.1, 2.2.0, 2.2.1, or 2.3.0 are configured with one or more config providers, and a connector is created/updated on that Connect cluster to use an externalized secret variable in a substring of a connector configuration property value (the externalized secret variable is not the whole configuration property value), then any client can issue a request to the same Connect cluster to obtain the connector's task configurations and the response will contain the plaintext secret rather than the externalized secrets variable. Users should upgrade to 2.2.2 or higher, or 2.3.1 or higher where this vulnerability has been fixed.", + "affected_purls": [ + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.0", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.1", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.2", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.3", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.4", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.5", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.6", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.7", + "qualifiers": "", + "subpath": "" + }, + { + "type": "apache", + "namespace": "", + "name": "kafka", + "version": "1.1.8", + "qualifiers": "", + "subpath": "" + } + ], + "fixed_purl": null, + "references": [ + { + "reference_id": "CVE-2019-12399", + "url": "https://kafka.apache.org/cve-list", + "severities": [] + }, + { + "reference_id": "CVE-2019-12399", + "url": "https://kafka.apache.org/cve-list#CVE-2019-12399", + "severities": [] + }, + { + "reference_id": "CVE-2019-12399", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2019-12399", + "severities": [] + } + ], + "weaknesses": [] + }, { "vulnerability_id": null, "aliases": [ From 85d3234cc4a8ebbfba688bbd8e8905ba41e54f87 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 26 Jul 2024 17:59:59 +0530 Subject: [PATCH 07/10] Properly parse Snyk fixed versions Signed-off-by: Keshav Priyadarshi --- vulntotal/datasources/snyk.py | 16 ++++++++-------- vulntotal/vulntotal_utils.py | 26 ++++++++++++++++++-------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/vulntotal/datasources/snyk.py b/vulntotal/datasources/snyk.py index 11734af6d..66a52f09b 100644 --- a/vulntotal/datasources/snyk.py +++ b/vulntotal/datasources/snyk.py @@ -8,6 +8,7 @@ # import logging +import re from typing import Iterable from urllib.parse import quote from urllib.parse import unquote_plus @@ -23,6 +24,8 @@ logger = logging.getLogger(__name__) +fixed_version_pattern = re.compile(r"\b\d[\w.-]*\b") + class SnykDataSource(DataSource): spdx_license_expression = "TODO" @@ -272,19 +275,16 @@ def parse_html_advisory(advisory_html, snyk_id, affected, purl) -> VendorData: advisory_soup = BeautifulSoup(advisory_html, "html.parser") cve_span = advisory_soup.find("span", class_="cve") if cve_span: - cve_anchor = cve_span.find("a", class_="vue--anchor") - if cve_anchor: + if cve_anchor := cve_span.find("a", class_="vue--anchor"): aliases.append(cve_anchor.get("id")) how_to_fix = advisory_soup.find( "div", class_="vue--block vuln-page__instruction-block vue--block--instruction" ) - if how_to_fix: - fixed = how_to_fix.find("p").text.split(" ") - if "Upgrade" in fixed: - lower = fixed.index("version") if "version" in fixed else fixed.index("versions") - upper = fixed.index("or") - fixed_versions = "".join(fixed[lower + 1 : upper]).split(",") + + if how_to_fix and (fixed := how_to_fix.find("p").text): + fixed_versions = fixed_version_pattern.findall(fixed) + aliases.append(snyk_id) return VendorData( purl=PackageURL(purl.type, purl.namespace, purl.name), diff --git a/vulntotal/vulntotal_utils.py b/vulntotal/vulntotal_utils.py index 79d866e05..d91ddc256 100644 --- a/vulntotal/vulntotal_utils.py +++ b/vulntotal/vulntotal_utils.py @@ -13,10 +13,9 @@ class GenericVersion: def __init__(self, version): - self.value = version.replace(" ", "").lstrip("v") - + self.value = version self.decomposed = tuple( - [int(com) if com.isnumeric() else com for com in self.value.split(".")] + [com for com in self.value.replace(" ", "").lstrip("vV").split(".")] ) def __str__(self): @@ -25,17 +24,28 @@ def __str__(self): def __eq__(self, other): if not isinstance(other, self.__class__): return NotImplemented - return self.value.__eq__(other.value) + for i, j in zip(self.decomposed, other.decomposed): + if i.isnumeric() and j.isnumeric(): + i = int(i) + j = int(j) + if not i.__eq__(j): + return False + return True def __lt__(self, other): if not isinstance(other, self.__class__): return NotImplemented for i, j in zip(self.decomposed, other.decomposed): - if not isinstance(i, type(j)): + if i.isnumeric() and j.isnumeric(): + i = int(i) + j = int(j) + if i.__eq__(j): continue + if i.__lt__(j): + return True if i.__gt__(j): return False - return True + return False def __le__(self, other): if not isinstance(other, self.__class__): @@ -57,8 +67,8 @@ def compare(version, package_comparator, package_version): "(": operator.gt, "[": operator.ge, } - compare = operator_comparator[package_comparator] - return compare(version, package_version) + compare_v = operator_comparator[package_comparator] + return compare_v(version, package_version) def parse_constraint(constraint): From 964275b55b8818d4435be40eca1c2634eb9f2d67 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 26 Jul 2024 18:03:36 +0530 Subject: [PATCH 08/10] Support cargo and pub in Snyk Signed-off-by: Keshav Priyadarshi --- vulntotal/datasources/snyk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vulntotal/datasources/snyk.py b/vulntotal/datasources/snyk.py index 66a52f09b..3aabfb416 100644 --- a/vulntotal/datasources/snyk.py +++ b/vulntotal/datasources/snyk.py @@ -110,6 +110,7 @@ def datasource_advisory_from_cve(self, cve: str) -> Iterable[VendorData]: @classmethod def supported_ecosystem(cls): return { + "cargo": "cargo", "cocoapods": "cocoapods", "composer": "composer", "golang": "golang", @@ -118,6 +119,7 @@ def supported_ecosystem(cls): "maven": "maven", "npm": "npm", "nuget": "nuget", + "pub": "pub", "pypi": "pip", "gem": "rubygems", # any purl.type not in supported_ecosystem shall implicitly be treated as unmanaged type From 96299a445e67f6d7275f0ec9c83f1a30e03dc3ad Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 26 Jul 2024 18:12:22 +0530 Subject: [PATCH 09/10] Use VersionRange.normalize to compare advisory Signed-off-by: Keshav Priyadarshi --- vulntotal/vulntotal_cli.py | 234 +++++++++++++++++-------------------- 1 file changed, 107 insertions(+), 127 deletions(-) diff --git a/vulntotal/vulntotal_cli.py b/vulntotal/vulntotal_cli.py index f820e7b2c..2c2326da7 100755 --- a/vulntotal/vulntotal_cli.py +++ b/vulntotal/vulntotal_cli.py @@ -19,11 +19,15 @@ # TODO: use saneyaml import yaml +from fetchcode import package_versions from packageurl import PackageURL from texttable import Texttable -from univers.normalized_range import NormalizedVersionRanges +from univers.version_range import RANGE_CLASS_BY_SCHEMES +from univers.version_range import VersionRange +from univers.version_range import build_range_from_github_advisory_constraint +from univers.version_range import build_range_from_snyk_advisory_string +from univers.version_range import from_gitlab_native -from vulnerabilities.package_managers import VERSION_API_CLASSES_BY_PACKAGE_TYPE from vulntotal.datasources import DATASOURCE_REGISTRY from vulntotal.validator import VendorData from vulntotal.vulntotal_utils import get_item @@ -189,7 +193,9 @@ def handler( prettyprint(purl, active_datasource, pagination, no_threading) elif purl: - prettyprint_group_by_cve(purl, active_datasource, pagination, no_threading, vers, no_compare) + prettyprint_group_by_cve( + purl, active_datasource, pagination, no_threading, vers, no_compare + ) def get_valid_datasources(datasources): @@ -281,10 +287,12 @@ def run_datasources(purl, datasources, no_threading=False): return vulnerabilities -class VulntotalEncoder(json.JSONEncoder): +class VendorDataEncoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, VendorData) or isinstance(obj, NormalizedVersionRanges): + if isinstance(obj, VendorData): return obj.to_dict() + if isinstance(obj, VersionRange): + return str(obj) return json.JSONEncoder.default(self, obj) @@ -298,7 +306,7 @@ def write_json_output(purl, datasources, json_output, no_threading, no_group, no grouped_by_cve = group_by_cve(vulnerabilities, PackageURL.from_string(purl), no_compare) results.update(grouped_by_cve) - return json.dump(results, json_output, cls=VulntotalEncoder, indent=2) + return json.dump(results, json_output, cls=VendorDataEncoder, indent=2) def noop(self, *args, **kw): @@ -316,30 +324,26 @@ def write_yaml_output(purl, datasources, yaml_output, no_threading, no_group, no results.update(vulnerabilities) else: grouped_by_cve = group_by_cve(vulnerabilities, PackageURL.from_string(purl), no_compare) - serialize_normalized_range(grouped_by_cve, no_compare) + serialize_version_range(grouped_by_cve, no_compare) results.update(grouped_by_cve) return yaml.dump(results, yaml_output, default_flow_style=False, indent=2, sort_keys=False) -def serialize_normalized_range(grouped_by_cve, no_compare): +def serialize_version_range(grouped_by_cve, no_compare): if no_compare: return for cve, value in grouped_by_cve.items(): if cve in ("NOCVE", "NOADVISORY"): continue - for datasource, resources in value.items(): + for _, resources in value.items(): for resource in resources: affected_versions = resource.get("normalized_affected_versions") fixed_versions = resource.get("normalized_fixed_versions") - if isinstance(affected_versions, NormalizedVersionRanges): - resource["normalized_affected_versions"] = [ - str(vers) for vers in affected_versions.version_ranges - ] - if isinstance(fixed_versions, NormalizedVersionRanges): - resource["normalized_fixed_versions"] = [ - str(vers) for vers in fixed_versions.version_ranges - ] + if isinstance(affected_versions, VersionRange): + resource["normalized_affected_versions"] = str(affected_versions) + if isinstance(fixed_versions, VersionRange): + resource["normalized_fixed_versions"] = str(fixed_versions) def prettyprint(purl, datasources, pagination, no_threading): @@ -363,17 +367,6 @@ def prettyprint(purl, datasources, pagination, no_threading): pydoc.pager(metadata + table.draw()) if pagination else click.echo(metadata + table.draw()) -NORMALIZED_VERSION_RANGE_BY_DATASOURCE = { - "deps": NormalizedVersionRanges.from_discrete, - "github": NormalizedVersionRanges.from_github, - "gitlab": NormalizedVersionRanges.from_gitlab, - "oss_index": None, - "osv": NormalizedVersionRanges.from_discrete, - "snyk": NormalizedVersionRanges.from_snyk, - "vulnerablecode": NormalizedVersionRanges.from_discrete, -} - - def group_by_cve(vulnerabilities, purl, no_compare): grouped_by_cve = {} nocve = {} @@ -382,32 +375,20 @@ def group_by_cve(vulnerabilities, purl, no_compare): if not advisories: if datasource not in noadvisory: noadvisory[datasource] = [] - noadvisory[datasource].append( - { - "advisory": None, - } - ) + noadvisory[datasource].append({"advisory": None}) for advisory in advisories: cve = next((x for x in advisory.aliases if x.startswith("CVE")), None) if not cve: if datasource not in nocve: nocve[datasource] = [] - nocve[datasource].append( - { - "advisory": advisory, - } - ) + nocve[datasource].append({"advisory": advisory}) continue if cve not in grouped_by_cve: grouped_by_cve[cve] = {} if datasource not in grouped_by_cve[cve]: grouped_by_cve[cve][datasource] = [] - grouped_by_cve[cve][datasource].append( - { - "advisory": advisory, - } - ) + grouped_by_cve[cve][datasource].append({"advisory": advisory}) grouped_by_cve["NOCVE"] = nocve grouped_by_cve["NOADVISORY"] = noadvisory if not no_compare: @@ -418,7 +399,6 @@ def group_by_cve(vulnerabilities, purl, no_compare): def normalize_version_ranges(grouped_by_cve, purl): package_versions = get_all_versions(purl) - for cve, value in grouped_by_cve.items(): if cve in ("NOCVE", "NOADVISORY"): continue @@ -427,20 +407,24 @@ def normalize_version_ranges(grouped_by_cve, purl): advisory = resource["advisory"] normalized_affected_versions = [] normalized_fixed_versions = [] - datasource_normalizer = NORMALIZED_VERSION_RANGE_BY_DATASOURCE.get(datasource) - if datasource_normalizer and advisory.affected_versions: + version_range_func = VERSION_RANGE_BY_DATASOURCE.get(datasource) + if version_range_func and advisory.affected_versions: + affected = advisory.affected_versions + if len(affected) == 1: + affected = affected[0] + try: - normalized_affected_versions = datasource_normalizer( - advisory.affected_versions, purl.type, package_versions - ) + vra = version_range_func(purl.type, affected) + normalized_affected_versions = vra.normalize(package_versions) except Exception as err: normalized_affected_versions = [err] if advisory.fixed_versions: try: - normalized_fixed_versions = NormalizedVersionRanges.from_discrete( - advisory.fixed_versions, purl.type, package_versions + vrf = get_range_from_discrete_version_string( + purl.type, advisory.fixed_versions ) + normalized_fixed_versions = vrf.normalize(package_versions) except Exception as err: normalized_fixed_versions = [err] @@ -449,35 +433,37 @@ def normalize_version_ranges(grouped_by_cve, purl): def compare(grouped_by_cve): - for cve, value in grouped_by_cve.items(): + for cve, advisories in grouped_by_cve.items(): if cve in ("NOCVE", "NOADVISORY"): continue - sources = list(value.keys()) + sources = list(advisories.keys()) board = {source: {} for source in sources} - """ - A typical board after comparison may look like this. - - board = { - "github":{ - "snyk": 0, - "gitlab": 1, - "deps": 0, - "vulnerablecode": 1, - "osv": 1, - "oss_index": 1, - }, - "snyk":{ - "github": 0, - "gitlab": 1, - "deps": 0, - "vulnerablecode": 1, - "osv": 1, - "oss_index": 1, - }, - ... - } - """ - for datasource, resources in value.items(): + + # For each unique CVE create the scoring board to score + # advisory from different datasources. + # A typical board after comparison may look like this. + + # board = { + # "github":{ + # "snyk": 0, + # "gitlab": 1, + # "deps": 0, + # "vulnerablecode": 1, + # "osv": 1, + # "oss_index": 1, + # }, + # "snyk":{ + # "github": 0, + # "gitlab": 1, + # "deps": 0, + # "vulnerablecode": 1, + # "osv": 1, + # "oss_index": 1, + # }, + # ... + # } + + for datasource, resources in advisories.items(): normalized_affected_versions_a = get_item(resources, 0, "normalized_affected_versions") normalized_fixed_versions_a = get_item(resources, 0, "normalized_fixed_versions") if normalized_fixed_versions_a and normalized_affected_versions_a: @@ -489,28 +475,31 @@ def compare(grouped_by_cve): ): continue normalized_affected_versions_b = get_item( - value, source, 0, "normalized_affected_versions" + advisories, source, 0, "normalized_affected_versions" ) normalized_fixed_versions_b = get_item( - value, source, 0, "normalized_fixed_versions" + advisories, source, 0, "normalized_fixed_versions" ) board[datasource][source] = 0 board[source][datasource] = 0 - if ( - normalized_fixed_versions_a == normalized_fixed_versions_b - and normalized_affected_versions_a == normalized_affected_versions_b - ): - board[datasource][source] = 1 - board[source][datasource] = 1 - - maximum = max([sum(list(table.values())) for table in board.values()]) + if normalized_fixed_versions_a == normalized_fixed_versions_b: + board[datasource][source] += 0.5 + board[source][datasource] += 0.5 + elif normalized_affected_versions_a == normalized_affected_versions_b: + board[datasource][source] += 0.5 + board[source][datasource] += 0.5 + + # Compute the relative score from the score board for each advisory. + maximum = max([sum(table.values()) for table in board.values()]) datasource_count = len(sources) for datasource, table in board.items(): if maximum == 0: - # NA if only one advisory else TC aka `Total Collision`. - value[datasource][0]["score"] = "TC" if datasource_count > 1 else "NA" + # NA if only one advisory and nothing to compare with. + # TC (Total Collision) i.e no two advisory agree on common fixed or affected version. + advisories[datasource][0]["score"] = "TC" if datasource_count > 1 else "NA" continue - value[datasource][0]["score"] = (sum(list(table.values())) / maximum) * 100 + datasource_score = (sum(table.values()) / maximum) * 100 + advisories[datasource][0]["score"] = datasource_score def prettyprint_group_by_cve(purl, datasources, pagination, no_threading, vers, no_compare): @@ -535,37 +524,21 @@ def prettyprint_group_by_cve(purl, datasources, pagination, no_threading, vers, if not no_compare and vers and "score" in resources[0]: na_affected = get_item(resources, 0, "normalized_affected_versions") na_fixed = get_item(resources, 0, "normalized_fixed_versions") - na_affected = ( - na_affected.version_ranges - if isinstance(na_affected, NormalizedVersionRanges) - else na_affected - ) - na_fixed = ( - na_fixed.version_ranges - if isinstance(na_fixed, NormalizedVersionRanges) - else na_fixed - ) - na_affected = "\n".join([str(i) for i in na_affected]) - na_fixed = "\n".join([str(i) for i in na_fixed]) table.add_row(["", "", "", na_affected, na_fixed, ""]) pydoc.pager(metadata + table.draw()) if pagination else click.echo(metadata + table.draw()) -def strip_leading_v(version): - if version.startswith("v"): - return version[1:] - return version - - def get_texttable(no_group=False, no_compare=False): quantum = 100 / 125 terminal_width = os.get_terminal_size().columns line_factor = terminal_width / 100 - column_5x = math.floor(5 * quantum * line_factor) - column_15x = math.floor(15 * quantum * line_factor) - column_20x = math.floor(20 * quantum * line_factor) + column_size = lambda f: math.floor(f * quantum * line_factor) + column_7x = column_size(5) + column_17x = column_size(10) + column_15x = column_size(15) + column_20x = column_size(20) table = Texttable() @@ -581,37 +554,44 @@ def get_texttable(no_group=False, no_compare=False): table.set_cols_dtype(["a", "a", "a", "a", "a"]) table.set_cols_align(["l", "l", "l", "l", "l"]) table.set_cols_valign(["t", "t", "t", "a", "t"]) - table.set_cols_width([column_20x, column_15x, column_20x, column_20x, column_20x]) + table.set_cols_width([column_15x, column_15x, column_20x, column_20x, column_20x]) table.header(["CVE", "DATASOURCE", "ALIASES", "AFFECTED", "FIXED"]) return table table.set_cols_dtype(["a", "a", "a", "a", "a", "a"]) table.set_cols_align(["l", "l", "l", "l", "l", "l"]) table.set_cols_valign(["t", "t", "t", "a", "t", "t"]) - table.set_cols_width([column_20x, column_15x, column_20x, column_20x, column_20x, column_5x]) + table.set_cols_width([column_17x, column_15x, column_15x, column_20x, column_20x, column_7x]) table.header(["CVE", "DATASOURCE", "ALIASES", "AFFECTED", "FIXED", "SCORE"]) return table -def get_all_versions(purl: PackageURL): - if purl.type not in VERSION_API_CLASSES_BY_PACKAGE_TYPE: - return +def get_range_from_discrete_version_string(schema, versions): + range_cls = RANGE_CLASS_BY_SCHEMES.get(schema) + if isinstance(versions, str): + versions = [versions] + return range_cls.from_versions(versions) - versionAPI = None - package_name = None - if purl.type == "maven": - package_name = f"{purl.namespace}:{purl.name}" - if purl.type in ("composer", "golang", "github"): - package_name = f"{purl.namespace}/{purl.name}" - if purl.type in ("nuget", "pypi", "gem", "npm", "hex", "deb", "cargo"): - package_name = purl.name +VERSION_RANGE_BY_DATASOURCE = { + "deps": get_range_from_discrete_version_string, + "github": build_range_from_github_advisory_constraint, + "gitlab": from_gitlab_native, + "oss_index": None, + "osv": get_range_from_discrete_version_string, + "snyk": build_range_from_snyk_advisory_string, + "safetydb": build_range_from_snyk_advisory_string, + "vulnerablecode": get_range_from_discrete_version_string, +} + - versionAPI = VERSION_API_CLASSES_BY_PACKAGE_TYPE.get(purl.type)() - all_versions = versionAPI.fetch(package_name) +def get_all_versions(purl: PackageURL): + if purl.type not in package_versions.SUPPORTED_ECOSYSTEMS: + return - return [strip_leading_v(package_version.value) for package_version in all_versions] + all_versions = package_versions.versions(str(purl)) + return [package_version.value for package_version in all_versions] if __name__ == "__main__": From 243f8497e6a3adfa0ce618ee1c13f2ceaabb901b Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Fri, 26 Jul 2024 19:17:59 +0530 Subject: [PATCH 10/10] Use integer column to display score Signed-off-by: Keshav Priyadarshi --- vulntotal/vulntotal_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vulntotal/vulntotal_cli.py b/vulntotal/vulntotal_cli.py index 2c2326da7..d60ceeb85 100755 --- a/vulntotal/vulntotal_cli.py +++ b/vulntotal/vulntotal_cli.py @@ -485,7 +485,7 @@ def compare(grouped_by_cve): if normalized_fixed_versions_a == normalized_fixed_versions_b: board[datasource][source] += 0.5 board[source][datasource] += 0.5 - elif normalized_affected_versions_a == normalized_affected_versions_b: + if normalized_affected_versions_a == normalized_affected_versions_b: board[datasource][source] += 0.5 board[source][datasource] += 0.5 @@ -535,8 +535,8 @@ def get_texttable(no_group=False, no_compare=False): line_factor = terminal_width / 100 column_size = lambda f: math.floor(f * quantum * line_factor) - column_7x = column_size(5) - column_17x = column_size(10) + column_7x = column_size(7) + column_17x = column_size(17) column_15x = column_size(15) column_20x = column_size(20) @@ -558,7 +558,7 @@ def get_texttable(no_group=False, no_compare=False): table.header(["CVE", "DATASOURCE", "ALIASES", "AFFECTED", "FIXED"]) return table - table.set_cols_dtype(["a", "a", "a", "a", "a", "a"]) + table.set_cols_dtype(["a", "a", "a", "a", "a", "i"]) table.set_cols_align(["l", "l", "l", "l", "l", "l"]) table.set_cols_valign(["t", "t", "t", "a", "t", "t"]) table.set_cols_width([column_17x, column_15x, column_15x, column_20x, column_20x, column_7x])