Skip to content

Commit

Permalink
Add: Support for NIST NVD CVE Change History API
Browse files Browse the repository at this point in the history
  • Loading branch information
n-thumann committed Nov 3, 2023
1 parent a4d68fd commit 1e2a6a1
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 0 deletions.
52 changes: 52 additions & 0 deletions pontos/nvd/cve_change_history/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright (C) 2023 Greenbone AG
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import asyncio
from argparse import ArgumentParser, Namespace
from typing import Callable

from pontos.nvd.cve_change_history.api import CVEChangeHistoryApi


__all__ = ("CVEChangeHistoryApi",)


async def query_changes(args: Namespace) -> None:
async with CVEChangeHistoryApi(token=args.token) as api:
async for cve in api.cve_changes(
cve_id=args.cve_id, event_name=args.event_name
):
print(cve)


def cve_changes() -> None:
parser = ArgumentParser()
parser.add_argument("--token", help="API key to use for querying.")
parser.add_argument("--cve-id", help="Get history for a specific CVE")
parser.add_argument(
"--event-name", help="Get all CVE associated with a specific event name"
)

main(parser, query_changes)


def main(parser: ArgumentParser, func: Callable) -> None:
try:
args = parser.parse_args()
asyncio.run(func(args))
except KeyboardInterrupt:
pass
152 changes: 152 additions & 0 deletions pontos/nvd/cve_change_history/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Copyright (C) 2023 Greenbone AG
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from datetime import datetime
from typing import Any, AsyncIterator, Dict, Iterable, Optional, Union

from httpx import Timeout

from pontos.nvd.api import (
DEFAULT_TIMEOUT_CONFIG,
NVDApi,
Params,
convert_camel_case,
format_date,
now,
)
from pontos.nvd.models.cve_change import CVEChange, EventName

__all__ = ("CVEChangeHistoryApi",)

DEFAULT_NIST_NVD_CVE_HISTORY_URL = (
"https://services.nvd.nist.gov/rest/json/cvehistory/2.0"
)


class CVEChangeHistoryApi(NVDApi):
"""
API for querying the NIST NVD CVE Change History information.
Should be used as an async context manager.
Example:
.. code-block:: python
from pontos.nvd.cve import CVEApi
async with CVEApi() as api:
cve = await api.cve("CVE-2022-45536")
"""

def __init__(
self,
*,
token: Optional[str] = None,
timeout: Optional[Timeout] = DEFAULT_TIMEOUT_CONFIG,
rate_limit: bool = True,
) -> None:
"""
Create a new instance of the CVE Change History API.
Args:
token: The API key to use. Using an API key allows to run more
requests at the same time.
timeout: Timeout settings for the HTTP requests
rate_limit: Set to False to ignore rate limits. The public rate
limit (without an API key) is 5 requests in a rolling 30 second
window. The rate limit with an API key is 50 requests in a
rolling 30 second window.
See https://nvd.nist.gov/developers/start-here#divRateLimits
Default: True.
"""
super().__init__(

Check warning on line 76 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L76

Added line #L76 was not covered by tests
DEFAULT_NIST_NVD_CVE_HISTORY_URL,
token=token,
timeout=timeout,
rate_limit=rate_limit,
)

async def cve_changes(
self,
*,
change_start_date: Optional[datetime] = None,
change_end_date: Optional[datetime] = None,
cve_id: Optional[str] = None,
event_name: Optional[EventName] = None,
) -> AsyncIterator[CVEChange]:
"""
Get all CVEs for the provided arguments
https://nvd.nist.gov/developers/vulnerabilities#divGetCves
Args:
TODO: ...
Returns:
An async iterator to iterate over CVEChange model instances
Example:
.. code-block:: python
from pontos.nvd.cve import CVEApi
async with CVEApi() as api:
async for cve in api.cves(keywords=["Mac OS X", "kernel"]):
print(cve.id)
"""
total_results: Optional[int] = None

Check warning on line 111 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L111

Added line #L111 was not covered by tests

params: Params = {}

Check warning on line 113 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L113

Added line #L113 was not covered by tests
if change_start_date:
params["changeStartDate"] = format_date(change_start_date)

Check warning on line 115 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L115

Added line #L115 was not covered by tests
if not change_end_date:
params["changeEndDate"] = format_date(now())

Check warning on line 117 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L117

Added line #L117 was not covered by tests

if change_end_date:
params["changeEndDate"] = format_date(change_end_date)

Check warning on line 120 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L120

Added line #L120 was not covered by tests

if cve_id:
params["cveId"] = cve_id

Check warning on line 123 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L123

Added line #L123 was not covered by tests

if event_name:
params["eventName"] = event_name

Check warning on line 126 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L126

Added line #L126 was not covered by tests

start_index: int = 0
results_per_page = None

Check warning on line 129 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L128-L129

Added lines #L128 - L129 were not covered by tests

while total_results is None or start_index < total_results:
params["startIndex"] = start_index

Check warning on line 132 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L132

Added line #L132 was not covered by tests

if results_per_page is not None:
params["resultsPerPage"] = results_per_page

Check warning on line 135 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L135

Added line #L135 was not covered by tests

response = await self._get(params=params)
response.raise_for_status()

Check warning on line 138 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L137-L138

Added lines #L137 - L138 were not covered by tests

data: Dict[str, Union[int, str, Dict[str, Any]]] = response.json(

Check warning on line 140 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L140

Added line #L140 was not covered by tests
object_hook=convert_camel_case
)

total_results = data["total_results"] # type: ignore
results_per_page: int = data["results_per_page"] # type: ignore
cve_changes: Iterable = data.get("cve_changes", []) # type: ignore

Check warning on line 146 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L144-L146

Added lines #L144 - L146 were not covered by tests

for cve_change in cve_changes:
yield CVEChange.from_dict(cve_change["change"])

Check warning on line 149 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L149

Added line #L149 was not covered by tests

if results_per_page is not None:
start_index += results_per_page

Check warning on line 152 in pontos/nvd/cve_change_history/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/nvd/cve_change_history/api.py#L152

Added line #L152 was not covered by tests
58 changes: 58 additions & 0 deletions pontos/nvd/models/cve_change.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright (C) 2023 Greenbone AG
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.


from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import List, Optional
from uuid import UUID

from pontos.models import Model


@dataclass
class EventName(str, Enum):
INITAL_ANALYSIS = "Initial Analysis"
REANALYSIS = "Reanalysis"
CVE_MODIFIED = "CVE Modified"
MODIFIED_ANALYSIS = "Modified Analysis"
CVE_TRANSLATED = "CVE Translated"
VENDOR_COMMENT = "Vendor Comment"
CVE_SOURCE_UPDATE = "CVE Source Update"
CPE_DEPRECATION_REMAP = "CPE Deprecation Remap"
CWE_REMAP = "CWE Remap"
CVE_REJECTED = "CVE Rejected"
CVE_UNREJECT = "CVE Unreject"


@dataclass
class Detail:
type: str
action: Optional[str] = None
old_value: Optional[str] = None
new_value: Optional[str] = None


@dataclass
class CVEChange(Model):
cve_id: str
event_name: EventName
cve_change_id: UUID
source_identifier: str
created: Optional[datetime] = None
details: Optional[List[Detail]] = None
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ pontos-github-actions = 'pontos.github.actions:main'
pontos-github-script = 'pontos.github.script:main'
pontos-nvd-cve = 'pontos.nvd.cve:cve_main'
pontos-nvd-cves = 'pontos.nvd.cve:cves_main'
pontos-nvd-cve-change-history = 'pontos.nvd.cve_change_history:cve_changes'
pontos-nvd-cpe = 'pontos.nvd.cpe:cpe_main'
pontos-nvd-cpes = 'pontos.nvd.cpe:cpes_main'

Expand Down

0 comments on commit 1e2a6a1

Please sign in to comment.