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

Dependabot Alert API #890

Merged
merged 3 commits into from
Oct 2, 2023
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: 2 additions & 0 deletions pontos/github/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .artifacts import GitHubAsyncRESTArtifacts
from .branch import GitHubAsyncRESTBranches, update_from_applied_settings
from .contents import GitHubAsyncRESTContent
from .dependabot import GitHubAsyncRESTDependabot
from .errors import GitHubApiError
from .helper import (
DEFAULT_GITHUB_API_URL,
Expand Down Expand Up @@ -47,6 +48,7 @@
"GitHubAsyncRESTArtifacts",
"GitHubAsyncRESTBranches",
"GitHubAsyncRESTContent",
"GitHubAsyncRESTDependabot",
"GitHubAsyncRESTLabels",
"GitHubAsyncRESTOrganizations",
"GitHubAsyncRESTPullRequests",
Expand Down
8 changes: 8 additions & 0 deletions pontos/github/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from pontos.github.api.branch import GitHubAsyncRESTBranches
from pontos.github.api.client import GitHubAsyncRESTClient
from pontos.github.api.contents import GitHubAsyncRESTContent
from pontos.github.api.dependabot import GitHubAsyncRESTDependabot
from pontos.github.api.helper import (
DEFAULT_GITHUB_API_URL,
DEFAULT_TIMEOUT_CONFIG,
Expand Down Expand Up @@ -99,6 +100,13 @@
"""
return GitHubAsyncRESTContent(self._client)

@property
def dependabot(self) -> GitHubAsyncRESTDependabot:
"""
Dependabot related API
"""
return GitHubAsyncRESTDependabot(self._client)

Check warning on line 108 in pontos/github/api/api.py

View check run for this annotation

Codecov / codecov/patch

pontos/github/api/api.py#L108

Added line #L108 was not covered by tests

@property
def labels(self) -> GitHubAsyncRESTLabels:
"""
Expand Down
326 changes: 326 additions & 0 deletions pontos/github/api/dependabot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
# SPDX-FileCopyrightText: 2023 Greenbone AG
#
# SPDX-License-Identifier: GPL-3.0-or-later


from typing import AsyncIterator, Optional, Union

from pontos.github.api.client import GitHubAsyncREST
from pontos.github.models.base import SortOrder
from pontos.github.models.dependabot import (
AlertSort,
AlertState,
DependabotAlert,
DependencyScope,
DismissedReason,
Severity,
)
from pontos.helper import enum_or_value


class GitHubAsyncRESTDependabot(GitHubAsyncREST):
async def _alerts(
self,
api: str,
*,
state: Union[AlertState, str, None] = None,
severity: Union[Severity, str, None] = None,
ecosystem: Optional[str] = None,
packages: Optional[list[str]] = None,
scope: Union[DependencyScope, str, None] = None,
sort: Union[AlertSort, str] = AlertSort.CREATED,
direction: Union[str, SortOrder] = SortOrder.DESC,
) -> AsyncIterator[DependabotAlert]:
params = {"per_page": "100"}

if state:
params["state"] = enum_or_value(state)
if severity:
params["severity"] = enum_or_value(severity)
if ecosystem:
params["ecosystem"] = ecosystem
if packages:
# as per REST api docu this param is passed as package (singular!)
params["package"] = ",".join(packages)

Check warning on line 44 in pontos/github/api/dependabot.py

View check run for this annotation

Codecov / codecov/patch

pontos/github/api/dependabot.py#L44

Added line #L44 was not covered by tests
if scope:
params["scope"] = enum_or_value(scope)
if sort:
params["sort"] = enum_or_value(sort)
if direction:
params["direction"] = enum_or_value(direction)

async for response in self._client.get_all(api, params=params):
for alert in response.json():
yield DependabotAlert.from_dict(alert)

async def enterprise_alerts(
self,
enterprise: str,
*,
state: Union[AlertState, str, None] = None,
severity: Union[Severity, str, None] = None,
ecosystem: Optional[str] = None,
packages: Optional[list[str]] = None,
scope: Union[DependencyScope, str, None] = None,
sort: Union[AlertSort, str] = AlertSort.CREATED,
direction: Union[str, SortOrder] = SortOrder.DESC,
) -> AsyncIterator[DependabotAlert]:
"""
Get the list of dependabot alerts for all repositories of a GitHub
enterprise

https://docs.github.com/en/rest/dependabot/alerts#list-dependabot-alerts-for-an-enterprise

Args:
enterprise: Name of the enterprise
state: Filter alerts by state
severity: Filter alerts by severity
ecosystem: Filter alerts by package ecosystem
package: Return alerts only for the provided packages
scope: Filter alerts by scope of the vulnerable dependency
sort: The property by which to sort the results. Default is to sort
alerts by creation date.
direction: The direction to sort the results by. Default is desc.

Raises:
HTTPStatusError: A httpx.HTTPStatusError is raised if the request
failed.

Returns:
An async iterator yielding the dependabot alerts

Example:
.. code-block:: python

from pontos.github.api import GitHubAsyncRESTApi

async with GitHubAsyncRESTApi(token) as api:
async for alert in api.dependabot.enterprise_alerts(
"my-enterprise"
):
print(alert)
"""

api = f"/enterprises/{enterprise}/dependabot/alerts"
async for alert in self._alerts(
api,
state=state,
severity=severity,
ecosystem=ecosystem,
packages=packages,
scope=scope,
sort=sort,
direction=direction,
):
yield alert

async def organization_alerts(
self,
organization: str,
*,
state: Union[AlertState, str, None] = None,
severity: Union[Severity, str, None] = None,
ecosystem: Optional[str] = None,
packages: Optional[list[str]] = None,
scope: Union[DependencyScope, str, None] = None,
sort: Union[AlertSort, str] = AlertSort.CREATED,
direction: Union[str, SortOrder] = SortOrder.DESC,
) -> AsyncIterator[DependabotAlert]:
"""
Get the list of dependabot alerts for all repositories of a GitHub
organization

https://docs.github.com/en/rest/dependabot/alerts#list-dependabot-alerts-for-an-organization

Args:
organization: Name of the organization
state: Filter alerts by state
severity: Filter alerts by severity
ecosystem: Filter alerts by package ecosystem
package: Return alerts only for the provided packages
scope: Filter alerts by scope of the vulnerable dependency
sort: The property by which to sort the results. Default is to sort
alerts by creation date.
direction: The direction to sort the results by. Default is desc.

Raises:
HTTPStatusError: A httpx.HTTPStatusError is raised if the request
failed.

Returns:
An async iterator yielding the dependabot alerts

Example:
.. code-block:: python

from pontos.github.api import GitHubAsyncRESTApi

async with GitHubAsyncRESTApi(token) as api:
async for alert in api.dependabot.organization_alerts(
"my-enterprise"
):
print(alert)
"""
api = f"/orgs/{organization}/dependabot/alerts"

async for alert in self._alerts(
api,
state=state,
severity=severity,
ecosystem=ecosystem,
packages=packages,
scope=scope,
sort=sort,
direction=direction,
):
yield alert

async def alerts(
self,
repo: str,
*,
state: Union[AlertState, str, None] = None,
severity: Union[Severity, str, None] = None,
ecosystem: Optional[str] = None,
packages: Optional[list[str]] = None,
scope: Union[DependencyScope, str, None] = None,
sort: Union[AlertSort, str] = AlertSort.CREATED,
direction: Union[str, SortOrder] = SortOrder.DESC,
) -> AsyncIterator[DependabotAlert]:
"""
Get the list of dependabot alerts for a repository

https://docs.github.com/en/rest/dependabot/alerts#list-dependabot-alerts-for-a-repository

Args:
repo: GitHub repository (owner/name)
state: Filter alerts by state
severity: Filter alerts by severity
ecosystem: Filter alerts by package ecosystem
package: Return alerts only for the provided packages
scope: Filter alerts by scope of the vulnerable dependency
sort: The property by which to sort the results. Default is to sort
alerts by creation date.
direction: The direction to sort the results by. Default is desc.

Raises:
HTTPStatusError: A httpx.HTTPStatusError is raised if the request
failed.

Returns:
An async iterator yielding the dependabot alerts

Example:
.. code-block:: python

from pontos.github.api import GitHubAsyncRESTApi

async with GitHubAsyncRESTApi(token) as api:
async for alert in api.dependabot.alerts(
"my-enterprise"
):
print(alert)
"""
api = f"/repos/{repo}/dependabot/alerts"

async for alert in self._alerts(
api,
state=state,
severity=severity,
ecosystem=ecosystem,
packages=packages,
scope=scope,
sort=sort,
direction=direction,
):
yield alert

async def alert(
self,
repo: str,
alert_number: Union[str, int],
) -> DependabotAlert:
"""
Get a single dependabot alert

https://docs.github.com/en/rest/dependabot/alerts#get-a-dependabot-alert

Args:
repo: GitHub repository (owner/name)
alert_number: The number that identifies a Dependabot alert in its
repository

Raises:
HTTPStatusError: A httpx.HTTPStatusError is raised if the request
failed.

Returns:
Dependabot alert information

Example:
.. code-block:: python

from pontos.github.api import GitHubAsyncRESTApi

async with GitHubAsyncRESTApi(token) as api:
alert = await api.dependabot.alert("foo/bar", 123)
"""
api = f"/repos/{repo}/dependabot/alerts/{alert_number}"
response = await self._client.get(api)
response.raise_for_status()
return DependabotAlert.from_dict(response.json())

async def update_alert(
self,
repo: str,
alert_number: Union[str, int],
state: Union[AlertState, str],
*,
dismissed_reason: Union[DismissedReason, str, None] = None,
dismissed_comment: str,
) -> DependabotAlert:
"""
Update a single dependabot alert

https://docs.github.com/en/rest/dependabot/alerts#update-a-dependabot-alert

Args:
repo: GitHub repository (owner/name)
alert_number: The number that identifies a Dependabot alert in its
repository
state: The state of the Dependabot alert
dismissed_reason: Required when state is dismissed. A reason for
dismissing the alert.
dismissed_comment: An optional comment associated with dismissing
the alert

Raises:
HTTPStatusError: A httpx.HTTPStatusError is raised if the request
failed.

Returns:
Dependabot alert information

Example:
.. code-block:: python

from pontos.github.api import GitHubAsyncRESTApi

async with GitHubAsyncRESTApi(token) as api:
alert = await api.dependabot.update(
"foo/bar",
123,
AlertState.FIXED,
)
"""
api = f"/repos/{repo}/dependabot/alerts/{alert_number}"

data = {"state": enum_or_value(state)}
if dismissed_comment:
data["dismissed_comment"] = dismissed_comment
if dismissed_reason:
data["dismissed_reason"] = enum_or_value(dismissed_reason)

response = await self._client.patch(api, data=data)
response.raise_for_status()
return DependabotAlert.from_dict(response.json())
14 changes: 14 additions & 0 deletions pontos/github/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"FileStatus",
"GitHubModel",
"Permission",
"SortOrder",
"Team",
"TeamPrivacy",
"TeamRole",
Expand Down Expand Up @@ -306,3 +307,16 @@ class Event(Enum):
WORKFLOW_CALL = "workflow_call"
WORKFLOW_DISPATCH = "workflow_dispatch"
WORKFLOW_RUN = "workflow_run"


class SortOrder(Enum):
"""
Sort order: asc or desc

Attributes:
ASC: Use ascending sort order
DESC: Use descending sort order
"""

ASC = "asc"
DESC = "desc"
Loading