Skip to content

Commit

Permalink
Add CVE command for SDK and CLI (#834)
Browse files Browse the repository at this point in the history
  • Loading branch information
bradchiappetta authored Jul 19, 2024
1 parent 3a7eb7d commit 28fb3d0
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 7 deletions.
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
advbumpversion==1.2.0
ipython==8.18.1;python_version>='3'
pre-commit==3.7.1
tox==4.15.1
tox==4.16.0
8 changes: 4 additions & 4 deletions requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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==3.2.5;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'
Expand All @@ -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'
yamllint==1.35.1;python_version>='3.8'
25 changes: 25 additions & 0 deletions src/greynoise/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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
40 changes: 38 additions & 2 deletions src/greynoise/cli/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
9 changes: 9 additions & 0 deletions src/greynoise/cli/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -237,5 +245,6 @@ def personadetails_formatter(results, verbose):
"sensor-activity": sensoractivity_formatter,
"sensor-list": sensorlist_formatter,
"persona-details": personadetails_formatter,
"cve": cvedetails_formatter,
},
}
20 changes: 20 additions & 0 deletions src/greynoise/cli/subcommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from greynoise.__version__ import __version__
from greynoise.cli.decorator import (
cve_command,
echo_result,
gnql_command,
handle_exceptions,
Expand Down Expand Up @@ -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
53 changes: 53 additions & 0 deletions src/greynoise/cli/templates/cvedetails.txt.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{% import "macros.txt.j2" as macros with context %}
{%- if results.details %}
----------------------------
<header>CVE Details</header>
----------------------------
<key>CVE</key>: <value>{{ results.id }}</value>
<key>Vuln Name</key>: <value>{{ results.details.vulnerability_name }}</value>
<key>Vuln Description</key>: <value>{{ results.details.vulnerability_description }}</value>
<key>Vendor</key>: <value>{{ results.details.vendor }}</value>
<key>Product</key>: <value>{{ results.details.product }}</value>
<key>CVSS Score</key>: <value>{{ results.details.cve_cvss_score }}</value>
<key>Published to NIST NVD</key>: <value>{{ results.details.published_to_nist_nvd }}</value>

{% if results.timeline -%}
<header>Timeline Information</header>
--------------------
<key>Published Date</key>: <value>{{ results.timeline.cve_published_date.split("T")[0] }}</value>
<key>Last Updated Date</key>: <value>{{ results.timeline.cve_last_updated_date.split("T")[0] }}</value>
<key>First Known Date</key>: <value>{{ results.timeline.first_known_published_date.split("T")[0] }}</value>
<key>Added to CISA Kev Date</key>: <value>{{ results.timeline.cisa_kev_date_added.split("T")[0] }}</value>
{%- endif %}

{% if results.exploitation_details -%}
<header>Exploitation Details</header>
--------------------
<key>Attack Vector</key>: <value>{{ results.exploitation_details.attack_vector }}</value>
<key>Exploit Found</key>: <value>{{ results.exploitation_details.exploit_found }}</value>
<key>Exploit in KEV</key>: <value>{{ results.exploitation_details.exploitation_registered_in_kev }}</value>
<key>EPSS Score</key>: <value>{{ results.exploitation_details.epss_score }}</value>
{%- endif %}

{% if results.exploitation_stats -%}
<header>Exploitation Stats</header>
------------------
<key># of Exploits</key>: <value>{{ results.exploitation_stats.number_of_available_exploits }}</value>
<key># of Threat Actors</key>: <value>{{ results.exploitation_stats.number_of_threat_actors_exploiting_vulnerability }}</value>
<key># of Botnets </key>: <value>{{ results.exploitation_stats.number_of_botnets_exploiting_vulnerability }}</value>
{%- endif %}

{% if results.exploitation_activity -%}
<header>Exploitation Activity</header>
---------------------
<key>Activity Seen</key>: <value>{{ results.exploitation_activity.activity_seen }}</value>
<key># Benign Scanners - 1 Day</key>: <value>{{ results.exploitation_activity.benign_ip_count_1d }}</value>
<key># Benign Scanners - 10 Days</key>: <value>{{ results.exploitation_activity.benign_ip_count_10d }}</value>
<key># Benign Scanners - 30 Days</key>: <value>{{ results.exploitation_activity.benign_ip_count_30d }}</value>
<key># Suspicious Scanners - 1 Day</key>: <value>{{ results.exploitation_activity.threat_ip_count_1d }}</value>
<key># Suspicious Scanners - 10 Day</key>: <value>{{ results.exploitation_activity.threat_ip_count_10d }}</value>
<key># Suspicious Scanners - 30 Day</key>: <value>{{ results.exploitation_activity.threat_ip_count_30d }}</value>
{%- endif %}
{% else %}
Provided CVE was not found or valid.
{%- endif %}
19 changes: 19 additions & 0 deletions src/greynoise/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Utility functions."""
import logging
import os
import re
from ipaddress import IPv6Address, ip_address

from six.moves.configparser import ConfigParser
Expand Down Expand Up @@ -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

0 comments on commit 28fb3d0

Please sign in to comment.