From 922259456d7e163106b93df7b6f09c3a0b3d9464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Ricks?= Date: Wed, 4 Oct 2023 15:39:02 +0200 Subject: [PATCH] Add: Implement GitHub API for secret scanning Allow to analyze the found secret scanning alerts of GitHub. --- pontos/github/api/api.py | 8 + pontos/github/api/secret_scanning.py | 374 ++++++++++ tests/github/api/test_secret_scanning.py | 831 +++++++++++++++++++++++ 3 files changed, 1213 insertions(+) create mode 100644 pontos/github/api/secret_scanning.py create mode 100644 tests/github/api/test_secret_scanning.py diff --git a/pontos/github/api/api.py b/pontos/github/api/api.py index 750dcde95..4703b3cdb 100644 --- a/pontos/github/api/api.py +++ b/pontos/github/api/api.py @@ -36,6 +36,7 @@ from pontos.github.api.release import GitHubAsyncRESTReleases from pontos.github.api.repositories import GitHubAsyncRESTRepositories from pontos.github.api.search import GitHubAsyncRESTSearch +from pontos.github.api.secret_scanning import GitHubAsyncRESTSecretScanning from pontos.github.api.tags import GitHubAsyncRESTTags from pontos.github.api.teams import GitHubAsyncRESTTeams from pontos.github.api.workflows import GitHubAsyncRESTWorkflows @@ -158,6 +159,13 @@ def repositories(self) -> GitHubAsyncRESTRepositories: """ return GitHubAsyncRESTRepositories(self._client) + @property + def secret_scanning(self) -> GitHubAsyncRESTSecretScanning: + """ + Secret scanning related API + """ + return GitHubAsyncRESTSecretScanning(self._client) + @property def teams(self) -> GitHubAsyncRESTTeams: """ diff --git a/pontos/github/api/secret_scanning.py b/pontos/github/api/secret_scanning.py new file mode 100644 index 000000000..d6498c90e --- /dev/null +++ b/pontos/github/api/secret_scanning.py @@ -0,0 +1,374 @@ +# SPDX-FileCopyrightText: 2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import AsyncIterator, Iterable, Optional, Union + +from pontos.github.api.client import GitHubAsyncREST +from pontos.github.models.base import SortOrder +from pontos.github.models.secret_scanning import ( + AlertLocation, + AlertSort, + AlertState, + CommitLocation, + IssueBodyLocation, + IssueCommentLocation, + IssueTitleLocation, + LocationType, + Resolution, + SecretScanningAlert, +) +from pontos.helper import enum_or_value + + +class GitHubAsyncRESTSecretScanning(GitHubAsyncREST): + async def _alerts( + self, + api: str, + *, + state: Union[AlertState, str, None] = None, + secret_types: Union[Iterable[str], None] = None, + resolutions: Union[Iterable[str], None] = None, + sort: Union[AlertSort, str] = AlertSort.CREATED, + direction: Union[str, SortOrder] = SortOrder.DESC, + ) -> AsyncIterator[SecretScanningAlert]: + params = {"per_page": "100"} + + if state: + params["state"] = enum_or_value(state) + if secret_types: + # as per REST api docu this param is passed as secret_type + params["secret_type"] = ",".join(secret_types) + if resolutions: + # as per REST api docu this param is passed as resolution + params["resolution"] = ",".join(resolutions) + 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 SecretScanningAlert.from_dict(alert) + + async def enterprise_alerts( + self, + enterprise: str, + *, + state: Union[AlertState, str, None] = None, + secret_types: Union[Iterable[str], None] = None, + resolutions: Union[Iterable[str], None] = None, + sort: Union[AlertSort, str] = AlertSort.CREATED, + direction: Union[str, SortOrder] = SortOrder.DESC, + ) -> AsyncIterator[SecretScanningAlert]: + """ + Get the list of secret scanning alerts for all repositories of a GitHub + enterprise + + https://docs.github.com/en/rest/secret-scanning/secret-scanning#list-secret-scanning-alerts-for-an-enterprise + + Args: + enterprise: Name of the enterprise + state: Filter alerts by state + secret_types: List of secret types to return. + resolutions: List secret scanning alerts with one of these provided + resolutions + 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 secret scanning alerts + + Example: + .. code-block:: python + + from pontos.github.api import GitHubAsyncRESTApi + + async with GitHubAsyncRESTApi(token) as api: + async for alert in api.secret_scanning.enterprise_alerts( + "my-enterprise" + ): + print(alert) + """ + + api = f"/enterprises/{enterprise}/secret-scanning/alerts" + async for alert in self._alerts( + api, + state=state, + secret_types=secret_types, + resolutions=resolutions, + sort=sort, + direction=direction, + ): + yield alert + + async def organization_alerts( + self, + organization: str, + *, + state: Union[AlertState, str, None] = None, + secret_types: Union[Iterable[str], None] = None, + resolutions: Union[Iterable[str], None] = None, + sort: Union[AlertSort, str] = AlertSort.CREATED, + direction: Union[str, SortOrder] = SortOrder.DESC, + ) -> AsyncIterator[SecretScanningAlert]: + """ + Get the list of secret scanning alerts for all repositories of a GitHub + organization + + https://docs.github.com/en/rest/secret-scanning/secret-scanning#list-secret-scanning-alerts-for-an-organization + + Args: + organization: Name of the organization + state: Filter alerts by state + secret_types: List of secret types to return. + resolutions: List secret scanning alerts with one of these provided + resolutions + 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 secret scanning alerts + + Example: + .. code-block:: python + + from pontos.github.api import GitHubAsyncRESTApi + + async with GitHubAsyncRESTApi(token) as api: + async for alert in api.secret_scanning.organization_alerts( + "my-enterprise" + ): + print(alert) + """ + + api = f"/orgs/{organization}/secret-scanning/alerts" + async for alert in self._alerts( + api, + state=state, + secret_types=secret_types, + resolutions=resolutions, + sort=sort, + direction=direction, + ): + yield alert + + async def alerts( + self, + repo: str, + *, + state: Union[AlertState, str, None] = None, + secret_types: Union[Iterable[str], None] = None, + resolutions: Union[Iterable[str], None] = None, + sort: Union[AlertSort, str] = AlertSort.CREATED, + direction: Union[str, SortOrder] = SortOrder.DESC, + ) -> AsyncIterator[SecretScanningAlert]: + """ + Get the list of secret scanning alerts for a repository + + https://docs.github.com/en/rest/secret-scanning/secret-scanning#list-secret-scanning-alerts-for-a-repository + + Args: + repo: GitHub repository (owner/name) + state: Filter alerts by state + secret_types: List of secret types to return. + resolutions: List secret scanning alerts with one of these provided + resolutions + 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 secret scanning alerts + + Example: + .. code-block:: python + + from pontos.github.api import GitHubAsyncRESTApi + + async with GitHubAsyncRESTApi(token) as api: + async for alert in api.secret_scanning.alerts( + "my-enterprise" + ): + print(alert) + """ + + api = f"/repos/{repo}/secret-scanning/alerts" + async for alert in self._alerts( + api, + state=state, + secret_types=secret_types, + resolutions=resolutions, + sort=sort, + direction=direction, + ): + yield alert + + async def alert( + self, + repo: str, + alert_number: Union[str, int], + ) -> SecretScanningAlert: + """ + Get a single secret scanning alert + + https://docs.github.com/en/rest/secret-scanning/secret-scanning#get-a-secret-scanning-alert + + Args: + repo: GitHub repository (owner/name) + alert_number: The number that identifies a secret scanning alert in + its repository + + Raises: + HTTPStatusError: A httpx.HTTPStatusError is raised if the request + failed. + + Returns: + Secret scanning alert information + + Example: + .. code-block:: python + + from pontos.github.api import GitHubAsyncRESTApi + + async with GitHubAsyncRESTApi(token) as api: + alert = await api.secret_scanning.alert("foo/bar", 123) + """ + api = f"/repos/{repo}/secret-scanning/alerts/{alert_number}" + response = await self._client.get(api) + response.raise_for_status() + return SecretScanningAlert.from_dict(response.json()) + + async def update_alert( + self, + repo: str, + alert_number: Union[str, int], + state: Union[AlertState, str], + *, + resolution: Union[Resolution, str, None] = None, + resolution_comment: Optional[str] = None, + ) -> SecretScanningAlert: + """ + Update a single secret scanning alert + + https://docs.github.com/en/rest/secret-scanning/secret-scanning#update-a-secret-scanning-alert + + Args: + repo: GitHub repository (owner/name) + alert_number: The number that identifies a secret scanning alert in + its repository + state: The state of the alert + resolution: Required when the state is resolved. The reason for + resolving the alert + resolution_comment: An optional comment when closing an alert. + Cannot be updated or deleted. + + Raises: + HTTPStatusError: A httpx.HTTPStatusError is raised if the request + failed. + + Returns: + Secret scanning 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.RESOLVED, + ) + """ + api = f"/repos/{repo}/secret-scanning/alerts/{alert_number}" + + data = {"state": enum_or_value(state)} + if resolution: + data["resolution"] = enum_or_value(resolution) + if resolution_comment: + data["resolution_comment"] = resolution_comment + + response = await self._client.patch(api, data=data) + response.raise_for_status() + return SecretScanningAlert.from_dict(response.json()) + + async def locations( + self, + repo: str, + alert_number: Union[str, int], + ) -> AsyncIterator[AlertLocation]: + """ + Lists all locations for a given secret scanning alert for an eligible + repository + + https://docs.github.com/en/rest/secret-scanning/secret-scanning#list-locations-for-a-secret-scanning-alert + + Args: + repo: GitHub repository (owner/name) + alert_number: The number that identifies a secret scanning alert in + its repository + + Raises: + HTTPStatusError: A httpx.HTTPStatusError is raised if the request + failed. + + Returns: + An async iterator yielding the secret scanning alert locations + + Example: + .. code-block:: python + + from pontos.github.api import GitHubAsyncRESTApi + + async with GitHubAsyncRESTApi(token) as api: + async for location in await api.secret_scanning.locations( + "foo/bar", + 123, + ): + print(location) + """ + api = f"/repos/{repo}/secret-scanning/alerts/{alert_number}/locations" + params = {"per_page": "100"} + + async for response in self._client.get_all(api, params=params): + for location in response.json(): + location_type = location["type"] + location_details = location["details"] + if location_type == LocationType.COMMIT.value: + yield AlertLocation( + type=LocationType.COMMIT, + details=CommitLocation.from_dict(location_details), + ) + elif location_type == LocationType.ISSUE_BODY.value: + yield AlertLocation( + type=LocationType.ISSUE_BODY, + details=IssueBodyLocation.from_dict(location_details), + ) + elif location_type == LocationType.ISSUE_COMMENT.value: + yield AlertLocation( + type=LocationType.ISSUE_COMMENT, + details=IssueCommentLocation.from_dict( + location_details + ), + ) + elif location_type == LocationType.ISSUE_TITLE.value: + yield AlertLocation( + type=LocationType.ISSUE_TITLE, + details=IssueTitleLocation.from_dict(location_details), + ) diff --git a/tests/github/api/test_secret_scanning.py b/tests/github/api/test_secret_scanning.py new file mode 100644 index 000000000..b0d95a065 --- /dev/null +++ b/tests/github/api/test_secret_scanning.py @@ -0,0 +1,831 @@ +# SPDX-FileCopyrightText: 2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ruff: noqa:E501 + +from pontos.github.api.secret_scanning import GitHubAsyncRESTSecretScanning +from pontos.github.models.base import SortOrder +from pontos.github.models.secret_scanning import ( + AlertSort, + AlertState, + LocationType, + Resolution, +) +from tests import AsyncIteratorMock, aiter, anext +from tests.github.api import GitHubAsyncRESTTestCase, create_response + +ALERTS = [ + { + "number": 2, + "created_at": "2020-11-06T18:48:51Z", + "url": "https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/2", + "html_url": "https://github.com/owner/private-repo/security/secret-scanning/2", + "locations_url": "https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/2/locations", + "state": "resolved", + "resolution": "false_positive", + "resolved_at": "2020-11-07T02:47:13Z", + "resolved_by": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://alambic.github.com/avatars/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": True, + }, + "secret_type": "adafruit_io_key", + "secret_type_display_name": "Adafruit IO Key", + "secret": "aio_XXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "repository": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": False, + }, + "private": False, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": False, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", + }, + "push_protection_bypassed_by": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://alambic.github.com/avatars/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": True, + }, + "push_protection_bypassed": True, + "push_protection_bypassed_at": "2020-11-06T21:48:51Z", + "resolution_comment": "Example comment", + }, + { + "number": 1, + "created_at": "2020-11-06T18:18:30Z", + "url": "https://api.github.com/repos/owner/repo/secret-scanning/alerts/1", + "html_url": "https://github.com/owner/repo/security/secret-scanning/1", + "locations_url": "https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/1/locations", + "state": "open", + "resolution": None, + "resolved_at": None, + "resolved_by": None, + "secret_type": "mailchimp_api_key", + "secret_type_display_name": "Mailchimp API Key", + "secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-us2", + "repository": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": False, + }, + "private": False, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": False, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", + }, + "push_protection_bypassed_by": None, + "push_protection_bypassed": False, + "push_protection_bypassed_at": None, + "resolution_comment": None, + }, +] + + +class GitHubAsyncRESTDependabotTestCase(GitHubAsyncRESTTestCase): + api_cls = GitHubAsyncRESTSecretScanning + + async def test_enterprise_alerts(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter(self.api.enterprise_alerts("foo")) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/enterprises/foo/secret-scanning/alerts", + params={"per_page": "100", "sort": "created", "direction": "desc"}, + ) + + async def test_enterprise_alerts_state(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.enterprise_alerts("foo", state=AlertState.RESOLVED) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/enterprises/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "state": "resolved", + }, + ) + + async def test_enterprise_alerts_secret_types(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.enterprise_alerts( + "foo", + secret_types=[ + "google_api_key", + "hashicorp_vault_service_token", + ], + ) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/enterprises/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "secret_type": "google_api_key,hashicorp_vault_service_token", + }, + ) + + async def test_enterprise_alerts_resolutions(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.enterprise_alerts( + "foo", + resolutions=["false_positive", "wont_fix", "revoked"], + ) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/enterprises/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "resolution": "false_positive,wont_fix,revoked", + }, + ) + + async def test_enterprise_alerts_sort(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.enterprise_alerts("foo", sort=AlertSort.UPDATED) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/enterprises/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "updated", + "direction": "desc", + }, + ) + + async def test_enterprise_alerts_direction(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.enterprise_alerts("foo", direction=SortOrder.ASC) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/enterprises/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "asc", + }, + ) + + async def test_organization_alerts(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter(self.api.organization_alerts("foo")) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/orgs/foo/secret-scanning/alerts", + params={"per_page": "100", "sort": "created", "direction": "desc"}, + ) + + async def test_organization_alerts_state(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.organization_alerts("foo", state=AlertState.RESOLVED) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/orgs/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "state": "resolved", + }, + ) + + async def test_organization_alerts_secret_types(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.organization_alerts( + "foo", + secret_types=[ + "google_api_key", + "hashicorp_vault_service_token", + ], + ) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/orgs/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "secret_type": "google_api_key,hashicorp_vault_service_token", + }, + ) + + async def test_organization_alerts_resolutions(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.organization_alerts( + "foo", + resolutions=["false_positive", "wont_fix", "revoked"], + ) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/orgs/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "resolution": "false_positive,wont_fix,revoked", + }, + ) + + async def test_organization_alerts_sort(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.organization_alerts("foo", sort=AlertSort.UPDATED) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/orgs/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "updated", + "direction": "desc", + }, + ) + + async def test_organization_alerts_direction(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.organization_alerts("foo", direction=SortOrder.ASC) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/orgs/foo/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "asc", + }, + ) + + async def test_alerts(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter(self.api.alerts("foo/bar")) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/repos/foo/bar/secret-scanning/alerts", + params={"per_page": "100", "sort": "created", "direction": "desc"}, + ) + + async def test_alerts_state(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter(self.api.alerts("foo/bar", state=AlertState.RESOLVED)) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/repos/foo/bar/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "state": "resolved", + }, + ) + + async def test_alerts_secret_types(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.alerts( + "foo/bar", + secret_types=[ + "google_api_key", + "hashicorp_vault_service_token", + ], + ) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/repos/foo/bar/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "secret_type": "google_api_key,hashicorp_vault_service_token", + }, + ) + + async def test_alerts_resolutions(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter( + self.api.alerts( + "foo/bar", + resolutions=["false_positive", "wont_fix", "revoked"], + ) + ) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/repos/foo/bar/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "desc", + "resolution": "false_positive,wont_fix,revoked", + }, + ) + + async def test_alerts_sort(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter(self.api.alerts("foo/bar", sort=AlertSort.UPDATED)) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/repos/foo/bar/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "updated", + "direction": "desc", + }, + ) + + async def test_alerts_direction(self): + response = create_response() + response.json.return_value = ALERTS + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter(self.api.alerts("foo/bar", direction=SortOrder.ASC)) + alert = await anext(async_it) + self.assertEqual(alert.number, 2) + alert = await anext(async_it) + self.assertEqual(alert.number, 1) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/repos/foo/bar/secret-scanning/alerts", + params={ + "per_page": "100", + "sort": "created", + "direction": "asc", + }, + ) + + async def test_update(self): + response = create_response() + response.json.return_value = { + "number": 42, + "created_at": "2020-11-06T18:18:30Z", + "url": "https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/42", + "html_url": "https://github.com/owner/private-repo/security/secret-scanning/42", + "locations_url": "https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/42/locations", + "state": "resolved", + "resolution": "used_in_tests", + "resolved_at": "2020-11-16T22:42:07Z", + "resolved_by": { + "login": "monalisa", + "id": 2, + "node_id": "MDQ6VXNlcjI=", + "avatar_url": "https://alambic.github.com/avatars/u/2?", + "gravatar_id": "", + "url": "https://api.github.com/users/monalisa", + "html_url": "https://github.com/monalisa", + "followers_url": "https://api.github.com/users/monalisa/followers", + "following_url": "https://api.github.com/users/monalisa/following{/other_user}", + "gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/monalisa/subscriptions", + "organizations_url": "https://api.github.com/users/monalisa/orgs", + "repos_url": "https://api.github.com/users/monalisa/repos", + "events_url": "https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url": "https://api.github.com/users/monalisa/received_events", + "type": "User", + "site_admin": True, + }, + "secret_type": "mailchimp_api_key", + "secret_type_display_name": "Mailchimp API Key", + "secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-us2", + "push_protection_bypassed": False, + "push_protection_bypassed_by": None, + "push_protection_bypassed_at": None, + "resolution_comment": "Example comment", + } + self.client.patch.return_value = response + + alert = await self.api.update_alert( + "foo/bar", + 1, + AlertState.RESOLVED, + resolution=Resolution.USED_IN_TESTS, + resolution_comment="Only used in tests", + ) + + self.client.patch.assert_awaited_once_with( + "/repos/foo/bar/secret-scanning/alerts/1", + data={ + "state": "resolved", + "resolution": "used_in_tests", + "resolution_comment": "Only used in tests", + }, + ) + + self.assertEqual(alert.number, 42) + self.assertIsNone(alert.repository) + + async def test_alerts_locations(self): + response = create_response() + response.json.return_value = [ + { + "type": "commit", + "details": { + "path": "/example/secrets.txt", + "start_line": 1, + "end_line": 1, + "start_column": 1, + "end_column": 64, + "blob_sha": "af5626b4a114abcb82d63db7c8082c3c4756e51b", + "blob_url": "https://api.github.com/repos/octocat/hello-world/git/blobs/af5626b4a114abcb82d63db7c8082c3c4756e51b", + "commit_sha": "f14d7debf9775f957cf4f1e8176da0786431f72b", + "commit_url": "https://api.github.com/repos/octocat/hello-world/git/commits/f14d7debf9775f957cf4f1e8176da0786431f72b", + }, + }, + { + "type": "issue_title", + "details": { + "issue_title_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347" + }, + }, + { + "type": "issue_body", + "details": { + "issue_body_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347" + }, + }, + { + "type": "issue_comment", + "details": { + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments/1081119451" + }, + }, + ] + + self.client.get_all.return_value = AsyncIteratorMock([response]) + + async_it = aiter(self.api.locations("foo/bar", 123)) + location = await anext(async_it) + self.assertEqual(location.type, LocationType.COMMIT) + location = await anext(async_it) + self.assertEqual(location.type, LocationType.ISSUE_TITLE) + location = await anext(async_it) + self.assertEqual(location.type, LocationType.ISSUE_BODY) + location = await anext(async_it) + self.assertEqual(location.type, LocationType.ISSUE_COMMENT) + + with self.assertRaises(StopAsyncIteration): + await anext(async_it) + + self.client.get_all.assert_called_once_with( + "/repos/foo/bar/secret-scanning/alerts/123/locations", + params={ + "per_page": "100", + }, + )