/ github / RepositoryAdvisory.py
RepositoryAdvisory.py
  1  ############################ Copyrights and license ############################
  2  #                                                                              #
  3  # Copyright 2023 Jonathan Leitschuh <Jonathan.Leitschuh@gmail.com>             #
  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  from __future__ import annotations
 23  
 24  from datetime import datetime
 25  from typing import TYPE_CHECKING, Any, Iterable
 26  
 27  import github.NamedUser
 28  from github.CWE import CWE
 29  from github.GithubObject import Attribute, NonCompletableGithubObject, NotSet, Opt
 30  from github.RepositoryAdvisoryCredit import Credit, RepositoryAdvisoryCredit
 31  from github.RepositoryAdvisoryCreditDetailed import RepositoryAdvisoryCreditDetailed
 32  from github.RepositoryAdvisoryVulnerability import AdvisoryVulnerability, RepositoryAdvisoryVulnerability
 33  
 34  if TYPE_CHECKING:
 35      from github.NamedUser import NamedUser
 36  
 37  
 38  class RepositoryAdvisory(NonCompletableGithubObject):
 39      """
 40      This class represents a RepositoryAdvisory.
 41      The reference can be found here https://docs.github.com/en/rest/security-advisories/repository-advisories
 42      """
 43  
 44      def _initAttributes(self) -> None:
 45          self._author: Attribute[NamedUser] = NotSet
 46          self._closed_at: Attribute[datetime] = NotSet
 47          self._created_at: Attribute[datetime] = NotSet
 48          self._credits: Attribute[list[RepositoryAdvisoryCredit]] = NotSet
 49          self._credits_detailed: Attribute[list[RepositoryAdvisoryCreditDetailed]] = NotSet
 50          self._cve_id: Attribute[str] = NotSet
 51          self._cwe_ids: Attribute[list[str]] = NotSet
 52          self._cwes: Attribute[list[CWE]] = NotSet
 53          self._description: Attribute[str] = NotSet
 54          self._ghsa_id: Attribute[str] = NotSet
 55          self._html_url: Attribute[str] = NotSet
 56          self._published_at: Attribute[datetime] = NotSet
 57          self._severity: Attribute[str] = NotSet
 58          self._state: Attribute[str] = NotSet
 59          self._summary: Attribute[str] = NotSet
 60          self._updated_at: Attribute[datetime] = NotSet
 61          self._url: Attribute[str] = NotSet
 62          self._vulnerabilities: Attribute[list[RepositoryAdvisoryVulnerability]] = NotSet
 63          self._withdrawn_at: Attribute[datetime] = NotSet
 64  
 65      def __repr__(self) -> str:
 66          return self.get__repr__({"ghsa_id": self.ghsa_id, "summary": self.summary})
 67  
 68      @property
 69      def author(self) -> NamedUser:
 70          return self._author.value
 71  
 72      @property
 73      def closed_at(self) -> datetime:
 74          return self._closed_at.value
 75  
 76      @property
 77      def created_at(self) -> datetime:
 78          return self._created_at.value
 79  
 80      @property
 81      def credits(
 82          self,
 83      ) -> list[RepositoryAdvisoryCredit]:
 84          return self._credits.value
 85  
 86      @property
 87      def credits_detailed(
 88          self,
 89      ) -> list[RepositoryAdvisoryCreditDetailed]:
 90          return self._credits_detailed.value
 91  
 92      @property
 93      def cve_id(self) -> str:
 94          return self._cve_id.value
 95  
 96      @property
 97      def cwe_ids(self) -> list[str]:
 98          return self._cwe_ids.value
 99  
100      @property
101      def cwes(self) -> list[CWE]:
102          return self._cwes.value
103  
104      @property
105      def description(self) -> str:
106          return self._description.value
107  
108      @property
109      def ghsa_id(self) -> str:
110          return self._ghsa_id.value
111  
112      @property
113      def html_url(self) -> str:
114          return self._html_url.value
115  
116      @property
117      def published_at(self) -> datetime:
118          return self._published_at.value
119  
120      @property
121      def severity(self) -> str:
122          return self._severity.value
123  
124      @property
125      def state(self) -> str:
126          return self._state.value
127  
128      @property
129      def summary(self) -> str:
130          return self._summary.value
131  
132      @property
133      def updated_at(self) -> datetime:
134          return self._updated_at.value
135  
136      @property
137      def url(self) -> str:
138          return self._url.value
139  
140      @property
141      def vulnerabilities(self) -> list[RepositoryAdvisoryVulnerability]:
142          return self._vulnerabilities.value
143  
144      @property
145      def withdrawn_at(self) -> datetime:
146          return self._withdrawn_at.value
147  
148      def add_vulnerability(
149          self,
150          ecosystem: str,
151          package_name: str | None = None,
152          vulnerable_version_range: str | None = None,
153          patched_versions: str | None = None,
154          vulnerable_functions: list[str] | None = None,
155      ) -> None:
156          """
157          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`\
158          """
159          return self.add_vulnerabilities(
160              [
161                  {
162                      "package": {
163                          "ecosystem": ecosystem,
164                          "name": package_name,
165                      },
166                      "vulnerable_version_range": vulnerable_version_range,
167                      "patched_versions": patched_versions,
168                      "vulnerable_functions": vulnerable_functions,
169                  }
170              ]
171          )
172  
173      def add_vulnerabilities(self, vulnerabilities: Iterable[AdvisoryVulnerability]) -> None:
174          """
175          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`
176          """
177          assert isinstance(vulnerabilities, Iterable), vulnerabilities
178          for vulnerability in vulnerabilities:
179              github.RepositoryAdvisoryVulnerability.RepositoryAdvisoryVulnerability._validate_vulnerability(
180                  vulnerability
181              )
182  
183          post_parameters = {
184              "vulnerabilities": [
185                  github.RepositoryAdvisoryVulnerability.RepositoryAdvisoryVulnerability._to_github_dict(vulnerability)
186                  for vulnerability in (self.vulnerabilities + list(vulnerabilities))
187              ]
188          }
189          headers, data = self._requester.requestJsonAndCheck(
190              "PATCH",
191              self.url,
192              input=post_parameters,
193          )
194          self._useAttributes(data)
195  
196      def offer_credit(
197          self,
198          login_or_user: str | github.NamedUser.NamedUser,
199          credit_type: str,
200      ) -> None:
201          """
202          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`
203          Offers credit to a user for a vulnerability in a repository.
204          Unless you are giving credit to yourself, the user having credit offered will need to explicitly accept the credit.
205          """
206          self.offer_credits([{"login": login_or_user, "type": credit_type}])
207  
208      def offer_credits(
209          self,
210          credited: Iterable[Credit],
211      ) -> None:
212          """
213          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`
214          Offers credit to a list of users for a vulnerability in a repository.
215          Unless you are giving credit to yourself, the user having credit offered will need to explicitly accept the credit.
216          :param credited: iterable of dict with keys "login" and "type"
217          """
218          assert isinstance(credited, Iterable), credited
219          for credit in credited:
220              RepositoryAdvisoryCredit._validate_credit(credit)
221  
222          patch_parameters = {
223              "credits": [RepositoryAdvisoryCredit._to_github_dict(credit) for credit in (self.credits + list(credited))]
224          }
225          headers, data = self._requester.requestJsonAndCheck(
226              "PATCH",
227              self.url,
228              input=patch_parameters,
229          )
230          self._useAttributes(data)
231  
232      def revoke_credit(self, login_or_user: str | github.NamedUser.NamedUser) -> None:
233          """
234          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`_
235          """
236          assert isinstance(login_or_user, (str, github.NamedUser.NamedUser)), login_or_user
237          if isinstance(login_or_user, github.NamedUser.NamedUser):
238              login_or_user = login_or_user.login
239          patch_parameters = {
240              "credits": [
241                  dict(login=credit.login, type=credit.type) for credit in self.credits if credit.login != login_or_user
242              ]
243          }
244          headers, data = self._requester.requestJsonAndCheck(
245              "PATCH",
246              self.url,
247              input=patch_parameters,
248          )
249          self._useAttributes(data)
250  
251      def clear_credits(self) -> None:
252          """
253          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`_
254          """
255          patch_parameters: dict[str, Any] = {"credits": []}
256          headers, data = self._requester.requestJsonAndCheck(
257              "PATCH",
258              self.url,
259              input=patch_parameters,
260          )
261          self._useAttributes(data)
262  
263      def edit(
264          self,
265          summary: Opt[str] = NotSet,
266          description: Opt[str] = NotSet,
267          severity_or_cvss_vector_string: Opt[str] = NotSet,
268          cve_id: Opt[str] = NotSet,
269          vulnerabilities: Opt[Iterable[AdvisoryVulnerability]] = NotSet,
270          cwe_ids: Opt[Iterable[str]] = NotSet,
271          credits: Opt[Iterable[Credit]] = NotSet,
272          state: Opt[str] = NotSet,
273      ) -> RepositoryAdvisory:
274          """
275          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`_
276          """
277          assert summary is NotSet or isinstance(summary, str), summary
278          assert description is NotSet or isinstance(description, str), description
279          assert severity_or_cvss_vector_string is NotSet or isinstance(
280              severity_or_cvss_vector_string, str
281          ), severity_or_cvss_vector_string
282          assert cve_id is NotSet or isinstance(cve_id, str), cve_id
283          assert vulnerabilities is NotSet or isinstance(vulnerabilities, Iterable), vulnerabilities
284          if isinstance(vulnerabilities, Iterable):
285              for vulnerability in vulnerabilities:
286                  github.RepositoryAdvisoryVulnerability.RepositoryAdvisoryVulnerability._validate_vulnerability(
287                      vulnerability
288                  )
289          assert cwe_ids is NotSet or (
290              isinstance(cwe_ids, Iterable) and all(isinstance(element, str) for element in cwe_ids)
291          ), cwe_ids
292          if isinstance(credits, Iterable):
293              for credit in credits:
294                  github.RepositoryAdvisoryCredit.RepositoryAdvisoryCredit._validate_credit(credit)
295          assert state is NotSet or isinstance(state, str), state
296          patch_parameters: dict[str, Any] = {}
297          if summary is not NotSet:
298              patch_parameters["summary"] = summary
299          if description is not NotSet:
300              patch_parameters["description"] = description
301          if isinstance(severity_or_cvss_vector_string, str):
302              if severity_or_cvss_vector_string.startswith("CVSS:"):
303                  patch_parameters["cvss_vector_string"] = severity_or_cvss_vector_string
304              else:
305                  patch_parameters["severity"] = severity_or_cvss_vector_string
306          if cve_id is not NotSet:
307              patch_parameters["cve_id"] = cve_id
308          if isinstance(vulnerabilities, Iterable):
309              patch_parameters["vulnerabilities"] = [
310                  github.RepositoryAdvisoryVulnerability.RepositoryAdvisoryVulnerability._to_github_dict(vulnerability)
311                  for vulnerability in vulnerabilities
312              ]
313          if isinstance(cwe_ids, Iterable):
314              patch_parameters["cwe_ids"] = list(cwe_ids)
315          if isinstance(credits, Iterable):
316              patch_parameters["credits"] = [
317                  github.RepositoryAdvisoryCredit.RepositoryAdvisoryCredit._to_github_dict(credit) for credit in credits
318              ]
319          if state is not NotSet:
320              patch_parameters["state"] = state
321          headers, data = self._requester.requestJsonAndCheck(
322              "PATCH",
323              self.url,
324              input=patch_parameters,
325          )
326          self._useAttributes(data)
327          return self
328  
329      def accept_report(self) -> None:
330          """
331          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`
332          Accepts the advisory reported from an external reporter via private vulnerability reporting.
333          """
334          patch_parameters = {"state": "draft"}
335          headers, data = self._requester.requestJsonAndCheck(
336              "PATCH",
337              self.url,
338              input=patch_parameters,
339          )
340          self._useAttributes(data)
341  
342      def publish(self) -> None:
343          """
344          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`
345          Publishes the advisory.
346          """
347          patch_parameters = {"state": "published"}
348          headers, data = self._requester.requestJsonAndCheck(
349              "PATCH",
350              self.url,
351              input=patch_parameters,
352          )
353          self._useAttributes(data)
354  
355      def close(self) -> None:
356          """
357          :calls: `PATCH /repos/{owner}/{repo}/security-advisories/:advisory_id <https://docs.github.com/en/rest/security-advisories/repository-advisories>`
358          Closes the advisory.
359          """
360          patch_parameters = {"state": "closed"}
361          headers, data = self._requester.requestJsonAndCheck(
362              "PATCH",
363              self.url,
364              input=patch_parameters,
365          )
366          self._useAttributes(data)
367  
368      def _useAttributes(self, attributes: dict[str, Any]) -> None:
369          if "author" in attributes:  # pragma no branch
370              self._author = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["author"])
371          if "closed_at" in attributes:  # pragma no branch
372              assert attributes["closed_at"] is None or isinstance(attributes["closed_at"], str), attributes["closed_at"]
373              self._closed_at = self._makeDatetimeAttribute(attributes["closed_at"])
374          if "created_at" in attributes:  # pragma no branch
375              assert attributes["created_at"] is None or isinstance(attributes["created_at"], str), attributes[
376                  "created_at"
377              ]
378              self._created_at = self._makeDatetimeAttribute(attributes["created_at"])
379          if "credits" in attributes:  # pragma no branch
380              self._credits = self._makeListOfClassesAttribute(
381                  RepositoryAdvisoryCredit,
382                  attributes["credits"],
383              )
384          if "credits_detailed" in attributes:  # pragma no branch
385              self._credits_detailed = self._makeListOfClassesAttribute(
386                  RepositoryAdvisoryCreditDetailed,
387                  attributes["credits_detailed"],
388              )
389          if "cve_id" in attributes:  # pragma no branch
390              self._cve_id = self._makeStringAttribute(attributes["cve_id"])
391          if "cwe_ids" in attributes:  # pragma no branch
392              self._cwe_ids = self._makeListOfStringsAttribute(attributes["cwe_ids"])
393          if "cwes" in attributes:  # pragma no branch
394              self._cwes = self._makeListOfClassesAttribute(CWE, attributes["cwes"])
395          if "description" in attributes:  # pragma no branch
396              self._description = self._makeStringAttribute(attributes["description"])
397          if "ghsa_id" in attributes:  # pragma no branch
398              self._ghsa_id = self._makeStringAttribute(attributes["ghsa_id"])
399          if "html_url" in attributes:  # pragma no branch
400              self._html_url = self._makeStringAttribute(attributes["html_url"])
401          if "published_at" in attributes:  # pragma no branch
402              assert attributes["published_at"] is None or isinstance(attributes["published_at"], str), attributes[
403                  "published_at"
404              ]
405              self._published_at = self._makeDatetimeAttribute(attributes["published_at"])
406          if "severity" in attributes:  # pragma no branch
407              self._severity = self._makeStringAttribute(attributes["severity"])
408          if "state" in attributes:  # pragma no branch
409              self._state = self._makeStringAttribute(attributes["state"])
410          if "summary" in attributes:  # pragma no branch
411              self._summary = self._makeStringAttribute(attributes["summary"])
412          if "updated_at" in attributes:  # pragma no branch
413              assert attributes["updated_at"] is None or isinstance(attributes["updated_at"], str), attributes[
414                  "updated_at"
415              ]
416              self._updated_at = self._makeDatetimeAttribute(attributes["updated_at"])
417          if "url" in attributes:  # pragma no branch
418              self._url = self._makeStringAttribute(attributes["url"])
419          if "vulnerabilities" in attributes:  # pragma no branch
420              self._vulnerabilities = self._makeListOfClassesAttribute(
421                  RepositoryAdvisoryVulnerability,
422                  attributes["vulnerabilities"],
423              )
424          if "withdrawn_at" in attributes:  # pragma no branch
425              assert attributes["withdrawn_at"] is None or isinstance(attributes["withdrawn_at"], str), attributes[
426                  "withdrawn_at"
427              ]
428              self._withdrawn_at = self._makeDatetimeAttribute(attributes["withdrawn_at"])