diff --git a/pontos/github/models/secret_scanning.py b/pontos/github/models/secret_scanning.py new file mode 100644 index 000000000..addfdd58c --- /dev/null +++ b/pontos/github/models/secret_scanning.py @@ -0,0 +1,189 @@ +# SPDX-FileCopyrightText: 2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from typing import Optional, Union + +from pontos.github.models.base import GitHubModel, User +from pontos.github.models.organization import Repository + + +class AlertSort(Enum): + """ + The property by which to sort the alerts + """ + + CREATED = "created" + UPDATED = "updated" + + +class AlertState(Enum): + """ + State of the GitHub Secrets Scanning Alert + """ + + OPEN = "open" + RESOLVED = "resolved" + + +class Resolution(Enum): + """ + The reason for resolving the alert + """ + + FALSE_POSITIVE = "false_positive" + WONT_FIX = "wont_fix" + REVOKED = "revoked" + USED_IN_TESTS = "used_in_tests" + + +class LocationType(Enum): + """ + Type of location + """ + + COMMIT = "commit" + ISSUE_TITLE = "issue_title" + ISSUE_BODY = "issue_body" + ISSUE_COMMENT = "issue_comment" + + +@dataclass +class SecretScanningAlert(GitHubModel): + """ + A GitHub Secret Scanning Alert + + Attributes: + number: The security alert number + url: The REST API URL of the alert resource + html_url: The GitHub URL of the alert resource + locations_url: The REST API URL of the code locations for this alert + state: Sets the state of the secret scanning alert. A `resolution` must + be provided when the state is set to `resolved`. + created_at: The time that the alert was created + updated_at: The time that the alert was last updated + resolution: Required when the `state` is `resolved` + resolved_at: The time that the alert was resolved + resolved_by: A GitHub user who resolved the alert + secret_type: The type of secret that secret scanning detected + secret_type_display_name: User-friendly name for the detected secret + secret: The secret that was detected + repository: The GitHub repository containing the alert. It's not + returned when requesting a specific alert + push_protection_bypassed: Whether push protection was bypassed for the + detected secret + push_protection_bypassed_by: A GitHub user who bypassed the push + protection + push_protection_bypassed_at: The time that push protection was bypassed + resolution_comment: The comment that was optionally added when this + alert was closed + """ + + number: int + url: str + html_url: str + locations_url: str + state: AlertState + secret_type: str + secret_type_display_name: str + secret: str + created_at: datetime + repository: Optional[Repository] = None + updated_at: Optional[datetime] = None + resolution: Optional[Resolution] = None + resolved_at: Optional[datetime] = None + resolved_by: Optional[User] = None + push_protection_bypassed: Optional[bool] = None + push_protection_bypassed_by: Optional[User] = None + push_protection_bypassed_at: Optional[datetime] = None + resolution_comment: Optional[str] = None + + +@dataclass +class CommitLocation(GitHubModel): + """ + Represents a 'commit' secret scanning location type + + Attributes: + path: The file path in the repository + start_line: Line number at which the secret starts in the file + end_line: Line number at which the secret ends in the file + start_column: The column at which the secret starts within the start + line + end_column: The column at which the secret ends within the end line + blob_sha: SHA-1 hash ID of the associated blob + blob_url: The API URL to get the associated blob resource + commit_sha: SHA-1 hash ID of the associated commit + commit_url: The API URL to get the associated commit resource + """ + + path: str + start_line: int + end_line: int + start_column: int + end_column: int + blob_sha: str + blob_url: str + commit_sha: str + commit_url: str + + +@dataclass +class IssueTitleLocation(GitHubModel): + """ + Represents an 'issue_title' secret scanning location type + + Attributes: + issue_title_url: The API URL to get the issue where the secret was + detected + """ + + issue_title_url: str + + +@dataclass +class IssueBodyLocation(GitHubModel): + """ + Represents an 'issue_body' secret scanning location type + + Attributes: + issue_body_url: The API URL to get the issue where the secret was + detected + """ + + issue_body_url: str + + +@dataclass +class IssueCommentLocation(GitHubModel): + """ + Represents an 'issue_comment' secret scanning location type + + Attributes: + issue_comment_url: he API URL to get the issue comment where the secret + was detected + """ + + issue_comment_url: str + + +@dataclass +class AlertLocation(GitHubModel): + """ + Location where the secret was detected + + Attributes: + type: The location type + details: Details about the location where the secret was detected + """ + + type: LocationType + details: Union[ + CommitLocation, + IssueTitleLocation, + IssueBodyLocation, + IssueCommentLocation, + ] diff --git a/tests/github/models/test_secret_scanning.py b/tests/github/models/test_secret_scanning.py new file mode 100644 index 000000000..e6710d4d5 --- /dev/null +++ b/tests/github/models/test_secret_scanning.py @@ -0,0 +1,182 @@ +# SPDX-FileCopyrightText: 2023 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ruff: noqa:E501 + +import unittest +from datetime import datetime, timezone + +from pontos.github.models.secret_scanning import ( + AlertState, + Resolution, + SecretScanningAlert, +) + +ALERT = { + "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", +} + + +class SecretScanningAlertTestCase(unittest.TestCase): + def test_from_dict(self): + alert = SecretScanningAlert.from_dict(ALERT) + + self.assertEqual(alert.number, 2) + self.assertEqual( + alert.url, + "https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/2", + ) + self.assertEqual( + alert.html_url, + "https://github.com/owner/private-repo/security/secret-scanning/2", + ) + self.assertEqual( + alert.locations_url, + "https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/2/locations", + ) + self.assertEqual(alert.state, AlertState.RESOLVED) + self.assertEqual(alert.secret_type, "adafruit_io_key") + self.assertEqual(alert.secret_type_display_name, "Adafruit IO Key") + self.assertEqual(alert.secret, "aio_XXXXXXXXXXXXXXXXXXXXXXXXXXXX") + self.assertEqual(alert.repository.id, 1296269) + self.assertEqual( + alert.created_at, + datetime(2020, 11, 6, 18, 48, 51, tzinfo=timezone.utc), + ) + self.assertIsNone(alert.updated_at) + + self.assertEqual(alert.resolution, Resolution.FALSE_POSITIVE) + self.assertEqual(alert.resolution_comment, "Example comment") + self.assertEqual( + alert.resolved_at, + datetime(2020, 11, 7, 2, 47, 13, tzinfo=timezone.utc), + ) + self.assertEqual(alert.resolved_by.login, "monalisa") + + self.assertTrue(alert.push_protection_bypassed) + self.assertEqual(alert.push_protection_bypassed_by.login, "monalisa") + self.assertEqual( + alert.push_protection_bypassed_at, + datetime(2020, 11, 6, 21, 48, 51, tzinfo=timezone.utc), + )