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: {{ 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 -%}
+
+--------------------
+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 -%}
+
+--------------------
+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 -%}
+
+------------------
+# 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 -%}
+
+---------------------
+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