From abd37760ae475efd13f779da2b296160311a1728 Mon Sep 17 00:00:00 2001 From: Brad Chiappetta <38439955+bradchiappetta@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:07:46 -0400 Subject: [PATCH] v2.3.0 Release (#842) --- .bumpversion.cfg | 2 +- CHANGELOG.rst | 19 ++++++- docs/source/conf.py | 2 +- requirements/common.txt | 2 +- requirements/dev.txt | 6 +-- requirements/docs.txt | 2 +- requirements/test.txt | 10 ++-- setup.py | 2 +- src/greynoise/__version__.py | 2 +- src/greynoise/api/__init__.py | 25 +++++++++ src/greynoise/cli/decorator.py | 40 +++++++++++++- src/greynoise/cli/formatter.py | 9 ++++ src/greynoise/cli/subcommand.py | 20 +++++++ src/greynoise/cli/templates/cvedetails.txt.j2 | 53 +++++++++++++++++++ src/greynoise/util.py | 19 +++++++ 15 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 src/greynoise/cli/templates/cvedetails.txt.j2 diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b05af50a..cbeefed0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.2.0 +current_version = 2.3.0 tag = False commit = False diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f61837d6..24d1c3fb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,22 @@ Changelog ========= +Version `2.3.0`_ +================ +**Date**: July 30, 2024 + +* API client: + + * Add `cve` command to query the CVE lookup API + +* CLI: + + * Add `cve` command to display result from CVE lookup API + +* Dependencies: + + * Updated cachetools to version 5.4.0 + Version `2.2.0`_ ================ **Date**: June 11, 2024 @@ -412,4 +428,5 @@ Version `0.2.0`_ .. _`2.0.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v1.3.0...2.0.0 .. _`2.0.1`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.0...2.0.1 .. _`2.1.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.0.1...2.1.0 -.. _`2.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.1.0...HEAD +.. _`2.2.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.1.0...2.2.0 +.. _`2.3.0`: https://github.com/GreyNoise-Intelligence/pygreynoise/compare/v2.2.0...HEAD diff --git a/docs/source/conf.py b/docs/source/conf.py index 6315bb2f..ffb920d2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -25,7 +25,7 @@ author = "GreyNoise Intelligence" # The full version, including alpha/beta/rc tags -release = "2.2.0" +release = "2.3.0" # -- General configuration --------------------------------------------------- diff --git a/requirements/common.txt b/requirements/common.txt index a4becad7..105f5d71 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -1,6 +1,6 @@ Click==8.1.7 ansimarkup==2.1.0 -cachetools==5.3.3;python_version>='3' +cachetools==5.4.0;python_version>='3' colorama==0.4.6 click-default-group==1.2.4 click-repl==0.3.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 0ec4cd5a..8a89b488 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ # Requirements needed to develop the application -r test.txt advbumpversion==1.2.0 -ipython==8.18.1;python_version>='3' -pre-commit==3.7.1 -tox==4.15.1 \ No newline at end of file +ipython==8.26.0;python_version>='3' +pre-commit==3.8.0 +tox==4.16.0 diff --git a/requirements/docs.txt b/requirements/docs.txt index f399794d..2d482b9b 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,5 +1,5 @@ # Requirements needed to build the documentation -r common.txt -Sphinx==7.3.7 +Sphinx==8.0.0 sphinx-click==6.0.0 sphinx-rtd-theme==2.0.0 \ No newline at end of file diff --git a/requirements/test.txt b/requirements/test.txt index 067270e2..e4c79e63 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -2,13 +2,13 @@ black<23.1.0;python_version<'3.7' black==23.3.0;python_version=='3.7' black==24.4.2;python_version>'3.7' flake8<5.0.4;python_version<'3.8' -flake8==7.0.0;python_version>='3.8' +flake8==7.1.0;python_version>='3.8' isort<5.12.0;python_version<'3.8' isort==5.13.2;python_version>='3.8' mock==5.1.0;python_version>='3.6' pylint<2.16.2;python_version=='3.6' # pyup: ignore -pylint==2.17.7;python_version=='3.7' -pylint==3.2.3;python_version>='3.8' +pylint<2.17.8;python_version=='3.7' +pylint==3.2.6;python_version>='3.8' pytest-cov==4.0.0;python_version=='3.6' pytest-cov==4.1.0;python_version=='3.7' pytest-cov==5.0.0;python_version>='3.8' @@ -17,7 +17,7 @@ pytest==7.4.4;python_version=='3.7' pytest==8.2.2;python_version>='3.8' restructuredtext-lint==1.4.0 twine<4.0.2;python_version<='3.7' -twine==5.1.0;python_version>'3.7' +twine==5.1.1;python_version>'3.7' yamllint==1.28.0;python_version=='3.6' yamllint==1.32.0;python_version=='3.7' -yamllint==1.35.1;python_version>='3.8' \ No newline at end of file +yamllint==1.35.1;python_version>='3.8' diff --git a/setup.py b/setup.py index 2783e07b..03094aea 100755 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def read(fname): setup( name="greynoise", - version="2.2.0", + version="2.3.0", description="Abstraction to interact with GreyNoise API.", url="https://greynoise.io/", author="GreyNoise Intelligence", diff --git a/src/greynoise/__version__.py b/src/greynoise/__version__.py index 14e777dc..3e33bfd8 100644 --- a/src/greynoise/__version__.py +++ b/src/greynoise/__version__.py @@ -5,4 +5,4 @@ __maintainer__ = "GreyNoise Intelligence" __email__ = "hello@greynoise.io" __status__ = "BETA" -__version__ = "2.2.0" +__version__ = "2.3.0" diff --git a/src/greynoise/api/__init__.py b/src/greynoise/api/__init__.py index bfac2328..79f0e196 100644 --- a/src/greynoise/api/__init__.py +++ b/src/greynoise/api/__init__.py @@ -15,6 +15,7 @@ from greynoise.exceptions import RateLimitError, RequestFailure from greynoise.util import ( load_config, + validate_cve_id, validate_ip, validate_similar_min_score, validate_timeline_days, @@ -61,6 +62,7 @@ class GreyNoise(object): # pylint: disable=R0205,R0902 EP_SENSOR_ACTIVITY = "v1/workspaces/{workspace_id}/sensors/activity" EP_SENSOR_LIST = "v1/workspaces/{workspace_id}/sensors" EP_PERSONA_DETAILS = "v1/personas/{persona_id}" + EP_CVE_LOOKUP = "v1/cve/{cve_id}" EP_ANALYZE_UPLOAD = "v2/analyze/upload" EP_ANALYZE = "v2/analyze/{id}" EP_NOT_IMPLEMENTED = "v2/request/{subcommand}" @@ -962,3 +964,26 @@ def persona_details(self, persona_id=None): response = self._request(endpoint) return response + + def cve(self, cve_id=None): + """Get CVE details by CVE ID + + :param cve_id: ID of CVE + :type cve_id: str + + + """ + if self.offering == "community": + response = { + "message": "CVE lookup is not supported with Community offering" + } + else: + LOGGER.debug("Getting Details for CVE ID: %s...", cve_id) + + # check if CVE submitted is in correct format + validate_cve_id(cve_id) + + endpoint = self.EP_CVE_LOOKUP.format(cve_id=cve_id) + response = self._request(endpoint) + + return response diff --git a/src/greynoise/cli/decorator.py b/src/greynoise/cli/decorator.py index bcb2ee67..96f9c306 100644 --- a/src/greynoise/cli/decorator.py +++ b/src/greynoise/cli/decorator.py @@ -240,7 +240,7 @@ def wrapper(api_client, *args, **kwargs): def workspace_command(function): - """Decorator that groups decorators common to sensor activity subcommands.""" + """Decorator that groups decorators common to workspace subcommands.""" @click.command() @click.argument("workspace_id", required=True) @@ -323,7 +323,7 @@ def wrapper(*args, **kwargs): def persona_command(function): - """Decorator that groups decorators common to sensor activity subcommands.""" + """Decorator that groups decorators common to persona subcommands.""" @click.command() @click.argument("persona_id", required=True) @@ -356,3 +356,39 @@ def wrapper(*args, **kwargs): return function(*args, **kwargs) return wrapper + + +def cve_command(function): + """Decorator that groups decorators common to cve subcommand.""" + + @click.command() + @click.argument("cve_id", required=True) + @click.option("-k", "--api-key", help="Key to include in API requests") + @click.option( + "-O", + "--offering", + help="Which API offering to use, enterprise or community, " + "defaults to enterprise", + ) + @click.option("-i", "--input", "input_file", type=click.File(), help="Input file") + @click.option( + "-o", "--output", "output_file", type=click.File(mode="w"), help="Output file" + ) + @click.option( + "-f", + "--format", + "output_format", + type=click.Choice(["json", "txt", "xml"]), + default="txt", + help="Output format", + ) + @click.option("-v", "--verbose", count=True, help="Verbose output") + @pass_api_client + @click.pass_context + @echo_result + @handle_exceptions + @functools.wraps(function) + def wrapper(*args, **kwargs): + return function(*args, **kwargs) + + return wrapper diff --git a/src/greynoise/cli/formatter.py b/src/greynoise/cli/formatter.py index 34ff954c..b22de69a 100644 --- a/src/greynoise/cli/formatter.py +++ b/src/greynoise/cli/formatter.py @@ -218,6 +218,14 @@ def personadetails_formatter(results, verbose): return template.render(results=results, verbose=verbose, max_width=max_width) +@colored_output +def cvedetails_formatter(results, verbose): + """Convert CVE Details to human-readable text.""" + template = JINJA2_ENV.get_template("cvedetails.txt.j2") + max_width, _ = shutil.get_terminal_size() + return template.render(results=results, verbose=verbose, max_width=max_width) + + FORMATTERS = { "json": json_formatter, "xml": xml_formatter, @@ -237,5 +245,6 @@ def personadetails_formatter(results, verbose): "sensor-activity": sensoractivity_formatter, "sensor-list": sensorlist_formatter, "persona-details": personadetails_formatter, + "cve": cvedetails_formatter, }, } diff --git a/src/greynoise/cli/subcommand.py b/src/greynoise/cli/subcommand.py index c27a3d23..b5de1d90 100644 --- a/src/greynoise/cli/subcommand.py +++ b/src/greynoise/cli/subcommand.py @@ -7,6 +7,7 @@ from greynoise.__version__ import __version__ from greynoise.cli.decorator import ( + cve_command, echo_result, gnql_command, handle_exceptions, @@ -497,3 +498,22 @@ def persona_details( persona_id=persona_id, ) return result + + +@cve_command +def cve( + context, + api_client, + api_key, + input_file, + output_file, + output_format, + verbose, + cve_id, + offering, +): + """Retrieve Details of a CVE.""" + result = api_client.cve( + cve_id=cve_id, + ) + return result diff --git a/src/greynoise/cli/templates/cvedetails.txt.j2 b/src/greynoise/cli/templates/cvedetails.txt.j2 new file mode 100644 index 00000000..7018691c --- /dev/null +++ b/src/greynoise/cli/templates/cvedetails.txt.j2 @@ -0,0 +1,53 @@ +{% import "macros.txt.j2" as macros with context %} +{%- if results.details %} +---------------------------- +
CVE Details
+---------------------------- +CVE: {{ results.id }} +Vuln Name: {{ results.details.vulnerability_name }} +Vuln Description: {{ results.details.vulnerability_description }} +Vendor: {{ results.details.vendor }} +Product: {{ results.details.product }} +CVSS Score: {{ results.details.cve_cvss_score }} +Published to NIST NVD: {{ results.details.published_to_nist_nvd }} + +{% if results.timeline -%} +
Timeline Information
+-------------------- +Published Date: {{ results.timeline.cve_published_date.split("T")[0] }} +Last Updated Date: {{ results.timeline.cve_last_updated_date.split("T")[0] }} +First Known Date: {{ results.timeline.first_known_published_date.split("T")[0] }} +Added to CISA Kev Date: {{ results.timeline.cisa_kev_date_added.split("T")[0] }} +{%- endif %} + +{% if results.exploitation_details -%} +
Exploitation Details
+-------------------- +Attack Vector: {{ results.exploitation_details.attack_vector }} +Exploit Found: {{ results.exploitation_details.exploit_found }} +Exploit in KEV: {{ results.exploitation_details.exploitation_registered_in_kev }} +EPSS Score: {{ results.exploitation_details.epss_score }} +{%- endif %} + +{% if results.exploitation_stats -%} +
Exploitation Stats
+------------------ +# of Exploits: {{ results.exploitation_stats.number_of_available_exploits }} +# of Threat Actors: {{ results.exploitation_stats.number_of_threat_actors_exploiting_vulnerability }} +# of Botnets : {{ results.exploitation_stats.number_of_botnets_exploiting_vulnerability }} +{%- endif %} + +{% if results.exploitation_activity -%} +
Exploitation Activity
+--------------------- +Activity Seen: {{ results.exploitation_activity.activity_seen }} +# Benign Scanners - 1 Day: {{ results.exploitation_activity.benign_ip_count_1d }} +# Benign Scanners - 10 Days: {{ results.exploitation_activity.benign_ip_count_10d }} +# Benign Scanners - 30 Days: {{ results.exploitation_activity.benign_ip_count_30d }} +# Suspicious Scanners - 1 Day: {{ results.exploitation_activity.threat_ip_count_1d }} +# Suspicious Scanners - 10 Day: {{ results.exploitation_activity.threat_ip_count_10d }} +# Suspicious Scanners - 30 Day: {{ results.exploitation_activity.threat_ip_count_30d }} +{%- endif %} +{% else %} +Provided CVE was not found or valid. +{%- endif %} diff --git a/src/greynoise/util.py b/src/greynoise/util.py index bb3a7d3e..d74bc85d 100644 --- a/src/greynoise/util.py +++ b/src/greynoise/util.py @@ -1,6 +1,7 @@ """Utility functions.""" import logging import os +import re from ipaddress import IPv6Address, ip_address from six.moves.configparser import ConfigParser @@ -228,3 +229,21 @@ def validate_similar_min_score(min_score): return True else: raise ValueError("Min Score must be a valid integer between 0 and 100.") + + +def validate_cve_id(cve_id): + """Check if provided value is a valid CVE ID + + :param cve_id: field value to validate. + :type cve_id: str + + """ + # CVE regular expression + cve_pattern = r"CVE-\d{4}-\d{4,7}" + + pattern = re.compile(cve_pattern) + + if not pattern.match(cve_id): + raise ValueError("The provided ID does not match the format: CVE-XXXX-YYYYY") + else: + return True