Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CVE command for SDK and CLI #834

Merged
merged 5 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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