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"])