Skip to content

Commit 6461909

Browse files
matt-davis27matt-davis27EnricoMi
authored
Add Secret Scanning Alerts and Improve Code Scan Alerts (#3307)
Fixes #2070 by adding the `security_and_analysis` _query parameters_ argument to [PATCH Repository](https://docs.github.com/en/rest/repos/repos#update-a-repository), which will allow programmatic enable/disable functionality for Secret Scanning, Code Scanning, and Dependabot Alerts. Adds functionality to [GET Secret Scanning Alerts](https://docs.github.com/en/rest/secret-scanning/secret-scanning) at the Organization and Repository levels. Also, updates _query parameters_ arguments for [GET Code Scan Alerts](https://docs.github.com/en/rest/code-scanning/code-scanning) and adds a new function at the Organization level to retrieve these alerts. Key Changes: - Adds `security_and_analysis` query parameter to the `edit()` function in **Repository.py** - Adds new functions `get_secret_scanning_alerts()` and `get_codescan_alerts()` in **Organization.py** - Adds new functions `get_secret_scanning_alerts()`, `get_secret_scanning_alert()`, and `get_codescan_alert()` in **Repository.py** - Updated function `get_codescan_alerts()` in **Repository.py** to include query parameters - Created new class objects `SecretScanAlert`, `SecretScanAlertInstance`, `OrganizationSecretScanAlert`, and `OrganizationCodeScanAlert` to support the above functions - Created tests `CodeScanAlert`, `SecretScanAlert`, `OrganizationCodeScanAlert`, `OrganizationSecretScanAlert`, `OrganizationDependabotAlert` to support the above functions - Updated tests `Repository` for the newest query parameter --------- Co-authored-by: matt-davis27 <[email protected]> Co-authored-by: Enrico Minack <[email protected]>
1 parent 95648db commit 6461909

37 files changed

+1666
-18
lines changed

github/CodeScanAlert.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# Copyright 2024 Jirka Borovec <[email protected]> #
99
# Copyright 2025 Enrico Minack <[email protected]> #
1010
# Copyright 2025 ReenigneArcher <[email protected]>#
11+
# Copyright 2025 Matthew Davis <[email protected]>#
1112
# #
1213
# This file is part of PyGithub. #
1314
# http://pygithub.readthedocs.io/ #
@@ -42,6 +43,9 @@
4243
from github.PaginatedList import PaginatedList
4344

4445
if TYPE_CHECKING:
46+
from github.CodeScanAlertInstance import CodeScanAlertInstance
47+
from github.CodeScanRule import CodeScanRule
48+
from github.CodeScanTool import CodeScanTool
4549
from github.NamedUser import NamedUser
4650
from github.Organization import Organization
4751

@@ -55,6 +59,7 @@ class CodeScanAlert(NonCompletableGithubObject):
5559
5660
The OpenAPI schema can be found at
5761
62+
- /components/schemas/code-scanning-alert
5863
- /components/schemas/code-scanning-alert-items
5964
6065
"""
@@ -64,22 +69,22 @@ def _initAttributes(self) -> None:
6469
self._created_at: Attribute[datetime] = NotSet
6570
self._dismissal_approved_by: Attribute[NamedUser | Organization] = NotSet
6671
self._dismissed_at: Attribute[datetime | None] = NotSet
67-
self._dismissed_by: Attribute[github.NamedUser.NamedUser | None] = NotSet
72+
self._dismissed_by: Attribute[NamedUser | None] = NotSet
6873
self._dismissed_comment: Attribute[str | None] = NotSet
6974
self._dismissed_reason: Attribute[str | None] = NotSet
7075
self._fixed_at: Attribute[datetime | None] = NotSet
7176
self._html_url: Attribute[str] = NotSet
7277
self._instances_url: Attribute[str] = NotSet
73-
self._most_recent_instance: Attribute[github.CodeScanAlertInstance.CodeScanAlertInstance] = NotSet
78+
self._most_recent_instance: Attribute[CodeScanAlertInstance] = NotSet
7479
self._number: Attribute[int] = NotSet
75-
self._rule: Attribute[github.CodeScanRule.CodeScanRule] = NotSet
80+
self._rule: Attribute[CodeScanRule] = NotSet
7681
self._state: Attribute[str] = NotSet
77-
self._tool: Attribute[github.CodeScanTool.CodeScanTool] = NotSet
82+
self._tool: Attribute[CodeScanTool] = NotSet
7883
self._updated_at: Attribute[datetime] = NotSet
7984
self._url: Attribute[str] = NotSet
8085

8186
def __repr__(self) -> str:
82-
return self.get__repr__({"number": self.number})
87+
return self.get__repr__({"number": self.number, "id": self.rule.id})
8388

8489
@property
8590
def assignees(self) -> list[NamedUser]:
@@ -98,7 +103,7 @@ def dismissed_at(self) -> datetime | None:
98103
return self._dismissed_at.value
99104

100105
@property
101-
def dismissed_by(self) -> github.NamedUser.NamedUser | None:
106+
def dismissed_by(self) -> NamedUser | None:
102107
return self._dismissed_by.value
103108

104109
@property
@@ -122,23 +127,23 @@ def instances_url(self) -> str:
122127
return self._instances_url.value
123128

124129
@property
125-
def most_recent_instance(self) -> github.CodeScanAlertInstance.CodeScanAlertInstance:
130+
def most_recent_instance(self) -> CodeScanAlertInstance:
126131
return self._most_recent_instance.value
127132

128133
@property
129134
def number(self) -> int:
130135
return self._number.value
131136

132137
@property
133-
def rule(self) -> github.CodeScanRule.CodeScanRule:
138+
def rule(self) -> CodeScanRule:
134139
return self._rule.value
135140

136141
@property
137142
def state(self) -> str:
138143
return self._state.value
139144

140145
@property
141-
def tool(self) -> github.CodeScanTool.CodeScanTool:
146+
def tool(self) -> CodeScanTool:
142147
return self._tool.value
143148

144149
@property
@@ -149,7 +154,7 @@ def updated_at(self) -> datetime:
149154
def url(self) -> str:
150155
return self._url.value
151156

152-
def get_instances(self) -> PaginatedList[github.CodeScanAlertInstance.CodeScanAlertInstance]:
157+
def get_instances(self) -> PaginatedList[CodeScanAlertInstance]:
153158
"""
154159
:calls: `GET /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}/instances <https://docs.github.com/en/rest/code-scanning/code-scanning#list-instances-of-a-code-scanning-alert>`_
155160
"""
@@ -187,7 +192,6 @@ def _useAttributes(self, attributes: dict[str, Any]) -> None:
187192
self._html_url = self._makeStringAttribute(attributes["html_url"])
188193
if "instances_url" in attributes: # pragma no branch
189194
self._instances_url = self._makeStringAttribute(attributes["instances_url"])
190-
191195
if "most_recent_instance" in attributes: # pragma no branch
192196
self._most_recent_instance = self._makeClassAttribute(
193197
github.CodeScanAlertInstance.CodeScanAlertInstance,
@@ -203,6 +207,5 @@ def _useAttributes(self, attributes: dict[str, Any]) -> None:
203207
self._tool = self._makeClassAttribute(github.CodeScanTool.CodeScanTool, attributes["tool"])
204208
if "updated_at" in attributes: # pragma no branch
205209
self._updated_at = self._makeDatetimeAttribute(attributes["updated_at"])
206-
207210
if "url" in attributes: # pragma no branch
208211
self._url = self._makeStringAttribute(attributes["url"])

github/CodeScanRule.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class CodeScanRule(NonCompletableGithubObject):
5555
5656
The OpenAPI schema can be found at
5757
58+
- /components/schemas/code-scanning-alert-rule
5859
- /components/schemas/code-scanning-alert-rule-summary
5960
6061
"""

github/Organization.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
# Copyright 2025 Enrico Minack <[email protected]> #
6868
# Copyright 2025 Pavel Abramov <[email protected]> #
6969
# Copyright 2025 Zachary <[email protected]> #
70+
# Copyright 2025 Matthew Davis <[email protected]>#
7071
# #
7172
# This file is part of PyGithub. #
7273
# http://pygithub.readthedocs.io/ #
@@ -102,9 +103,11 @@
102103
import github.GithubObject
103104
import github.HookDelivery
104105
import github.NamedUser
106+
import github.OrganizationCodeScanAlert
105107
import github.OrganizationCustomProperty
106108
import github.OrganizationDependabotAlert
107109
import github.OrganizationSecret
110+
import github.OrganizationSecretScanAlert
108111
import github.OrganizationVariable
109112
import github.Plan
110113
import github.Project
@@ -139,13 +142,15 @@
139142
from github.Label import Label
140143
from github.Migration import Migration
141144
from github.NamedUser import NamedUser, OrganizationInvitation
145+
from github.OrganizationCodeScanAlert import OrganizationCodeScanAlert
142146
from github.OrganizationCustomProperty import (
143147
CustomProperty,
144148
OrganizationCustomProperty,
145149
RepositoryCustomPropertyValues,
146150
)
147151
from github.OrganizationDependabotAlert import OrganizationDependabotAlert
148152
from github.OrganizationSecret import OrganizationSecret
153+
from github.OrganizationSecretScanAlert import OrganizationSecretScanAlert
149154
from github.OrganizationVariable import OrganizationVariable
150155
from github.Plan import Plan
151156
from github.Project import Project
@@ -1621,6 +1626,134 @@ def get_dependabot_alerts(
16211626
url_parameters,
16221627
)
16231628

1629+
def get_codescan_alerts(
1630+
self,
1631+
tool_name: Opt[str] = NotSet,
1632+
tool_guid: Opt[str] = NotSet,
1633+
ref: Opt[str] = NotSet,
1634+
pr: Opt[int] = NotSet,
1635+
sort: Opt[str] = NotSet,
1636+
direction: Opt[str] = NotSet,
1637+
state: Opt[str] = NotSet,
1638+
severity: Opt[str] = NotSet,
1639+
) -> PaginatedList[OrganizationCodeScanAlert]:
1640+
"""
1641+
:calls: `GET /orgs/{org}/code-scanning/alerts <https://docs.github.com/en/rest/code-scanning/code-scanning#list-code-scanning-alerts-for-an-organization>`_
1642+
:param tool_name: Optional string
1643+
:param tool_guid: Optional string
1644+
:param ref: Optional string
1645+
:param pr: Optional integer
1646+
:param sort: Optional string
1647+
:param direction: Optional string
1648+
:param state: Optional string
1649+
:param severity: Optional string
1650+
:rtype: :class:`PaginatedList` of :class:`github.CodeScanAlert.CodeScanAlert`
1651+
"""
1652+
allowed_sorts = ["created", "updated"]
1653+
allowed_directions = ["asc", "desc"]
1654+
allowed_states = ["open", "closed", "dismissed", "fixed"]
1655+
allowed_severities = ["critical", "high", "medium", "low", "warning", "note", "error"]
1656+
assert is_optional(tool_name, str), tool_name
1657+
assert is_optional(tool_guid, str), tool_guid
1658+
assert (
1659+
tool_name is NotSet or tool_guid is NotSet
1660+
), "You can specify the tool by using either tool_guid or tool_name, but not both."
1661+
assert is_optional(ref, str), ref
1662+
assert is_optional(pr, int), pr
1663+
assert sort in allowed_sorts + [NotSet], f"Sort can be one of {', '.join(allowed_sorts)}"
1664+
assert direction in allowed_directions + [NotSet], f"Direction can be one of {', '.join(allowed_directions)}"
1665+
assert state in allowed_states + [NotSet], f"State can be one of {', '.join(allowed_states)}"
1666+
assert severity in allowed_severities + [NotSet], f"Severity can be one of {', '.join(allowed_severities)}"
1667+
url_parameters = NotSet.remove_unset_items(
1668+
{
1669+
"tool_name": tool_name,
1670+
"tool_guid": tool_guid,
1671+
"ref": ref,
1672+
"pr": pr,
1673+
"sort": sort,
1674+
"direction": direction,
1675+
"state": state,
1676+
"severity": severity,
1677+
}
1678+
)
1679+
return PaginatedList(
1680+
github.OrganizationCodeScanAlert.OrganizationCodeScanAlert,
1681+
self._requester,
1682+
f"{self.url}/code-scanning/alerts",
1683+
url_parameters,
1684+
)
1685+
1686+
def get_secret_scanning_alerts(
1687+
self,
1688+
state: Opt[str] = NotSet,
1689+
secret_type: Opt[str] = NotSet,
1690+
resolution: Opt[str] = NotSet,
1691+
sort: Opt[str] = NotSet,
1692+
direction: Opt[str] = NotSet,
1693+
validity: Opt[str] = NotSet,
1694+
is_publicly_leaked: Opt[bool] = NotSet,
1695+
is_multi_repo: Opt[bool] = NotSet,
1696+
hide_secret: Opt[bool] = NotSet,
1697+
) -> PaginatedList[OrganizationSecretScanAlert]:
1698+
"""
1699+
:calls: `GET /orgs/{org}/secret-scanning/alerts <https://docs.github.com/en/rest/secret-scanning/secret-scanning#list-secret-scanning-alerts-for-an-organization>`_
1700+
:param state: Optional string
1701+
:param secret_type: Optional string
1702+
:param resolution: Optional string
1703+
:param sort: Optional string
1704+
:param direction: Optional string
1705+
:param validity: Optional string
1706+
:param is_publicly_leaked: Optional bool
1707+
:param is_multi_repo: Optional bool
1708+
:param hide_secret: Optional bool
1709+
:rtype: :class:`PaginatedList` of :class:`github.SecretScanAlert.SecretScanAlert`
1710+
"""
1711+
allowed_states = ["open", "resolved"]
1712+
# allowed_secret_types = ["http_basic_authentication_header", "http_bearer_authentication_header", ...]
1713+
allowed_resolutions = [
1714+
"false_positive",
1715+
"wont_fix",
1716+
"revoked",
1717+
"pattern_edited",
1718+
"pattern_deleted",
1719+
"used_in_tests",
1720+
]
1721+
allowed_sorts = ["created", "updated"]
1722+
allowed_directions = ["asc", "desc"]
1723+
allowed_validities = ["active", "inactive", "unknown"]
1724+
assert state in allowed_states + [NotSet], f"State can be one of {', '.join(allowed_states)}"
1725+
# assert secret_type in allowed_secret_types + [NotSet], \
1726+
# "Secret_type can be one of the tokens listed on \
1727+
# https://docs.github.com/en/code-security/secret-scanning/introduction/supported-secret-scanning-patterns#supported-secrets"
1728+
assert resolution in allowed_resolutions + [
1729+
NotSet
1730+
], f"Resolution can be one of {', '.join(allowed_resolutions)}"
1731+
assert sort in allowed_sorts + [NotSet], f"Sort can be one of {', '.join(allowed_sorts)}"
1732+
assert direction in allowed_directions + [NotSet], f"Direction can be one of {', '.join(allowed_directions)}"
1733+
assert validity in allowed_validities + [NotSet], f"Validity can be one of {', '.join(allowed_validities)}"
1734+
assert is_optional(is_publicly_leaked, bool), is_publicly_leaked
1735+
assert is_optional(is_multi_repo, bool), is_multi_repo
1736+
assert is_optional(hide_secret, bool), hide_secret
1737+
url_parameters = NotSet.remove_unset_items(
1738+
{
1739+
"state": state,
1740+
"secret_type": secret_type,
1741+
"resolution": resolution,
1742+
"sort": sort,
1743+
"direction": direction,
1744+
"validity": validity,
1745+
"is_publicly_leaked": is_publicly_leaked,
1746+
"is_multi_repo": is_multi_repo,
1747+
"hide_secret": hide_secret,
1748+
}
1749+
)
1750+
return PaginatedList(
1751+
github.OrganizationSecretScanAlert.OrganizationSecretScanAlert,
1752+
self._requester,
1753+
f"{self.url}/secret-scanning/alerts",
1754+
url_parameters,
1755+
)
1756+
16241757
def get_custom_properties(self) -> PaginatedList[OrganizationCustomProperty]:
16251758
"""
16261759
:calls: `GET /orgs/{org}/properties/schema <https://docs.github.com/en/rest/orgs/custom-properties#get-all-custom-properties-for-an-organization>`_
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
############################ Copyrights and license ############################
2+
# #
3+
# Copyright 2025 Matthew Davis <[email protected]>#
4+
# #
5+
# This file is part of PyGithub. #
6+
# http://pygithub.readthedocs.io/ #
7+
# #
8+
# PyGithub is free software: you can redistribute it and/or modify it under #
9+
# the terms of the GNU Lesser General Public License as published by the Free #
10+
# Software Foundation, either version 3 of the License, or (at your option) #
11+
# any later version. #
12+
# #
13+
# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY #
14+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
15+
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more #
16+
# details. #
17+
# #
18+
# You should have received a copy of the GNU Lesser General Public License #
19+
# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. #
20+
# #
21+
################################################################################
22+
23+
from __future__ import annotations
24+
25+
from typing import Any
26+
27+
from github.CodeScanAlert import CodeScanAlert
28+
from github.GithubObject import Attribute, NotSet
29+
from github.Repository import Repository
30+
31+
32+
class OrganizationCodeScanAlert(CodeScanAlert):
33+
"""
34+
This class represents a Code Scan Alerts for an organization.
35+
36+
The reference can be found here
37+
https://docs.github.com/en/rest/code-scanning/code-scanning#list-code-scanning-alerts-for-an-organization
38+
39+
The OpenAPI schema can be found at
40+
41+
- /components/schemas/code-scanning-organization-alert-items
42+
43+
"""
44+
45+
def _initAttributes(self) -> None:
46+
super()._initAttributes()
47+
self._repository: Attribute[Repository] = NotSet
48+
49+
@property
50+
def repository(self) -> Repository:
51+
return self._repository.value
52+
53+
def _useAttributes(self, attributes: dict[str, Any]) -> None:
54+
super()._useAttributes(attributes)
55+
if "repository" in attributes:
56+
self._repository = self._makeClassAttribute(Repository, attributes["repository"])

0 commit comments

Comments
 (0)