-
-
Notifications
You must be signed in to change notification settings - Fork 2
Automatically credit remediation developers and reviewers #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1d89775
27dd5d5
3593cbf
4ca76f6
f5f5de9
68b965d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,6 +96,75 @@ def get_repository_advisories( | |
| raise RuntimeError("Request to paginate advisories failed.") | ||
|
|
||
|
|
||
| def get_security_advisory_credits( | ||
| github: GitHub, | ||
| security_advisory: dict[str, typing.Any], | ||
| ) -> list[dict[str, str]]: | ||
| """Generates a list of credits to apply to a security | ||
| advisory, such as developing or reviewing a remediation. | ||
| Respects credits that already exist on an advisory. | ||
| """ | ||
| credits = (security_advisory.get("credits", None) or [])[:] | ||
|
|
||
| def credit_if_uncredited(login: str, type: str) -> None: | ||
| # GHSA only allows one credit type per user, | ||
| # so we don't want to overwrite existing credits. | ||
| if any(c["login"].lower() == login.lower() for c in credits): | ||
| return | ||
| credits.append( | ||
| { | ||
| "login": login, | ||
| "type": type, | ||
| } | ||
| ) | ||
|
|
||
| if (private_fork := security_advisory.get("private_fork")) is not None: | ||
| private_fork_owner = private_fork["owner"]["login"] | ||
| private_fork_repo = private_fork["name"] | ||
|
|
||
| try: | ||
| # Pagination shouldn't be necessary here, there isn't likely | ||
| # to be more than 100 pull requests on a single GHSA private repo. | ||
| # Usually there'll be 2 at most. | ||
| pull_requests = json.loads( | ||
| github.rest.pulls.list( | ||
| owner=private_fork_owner, | ||
| repo=private_fork_repo, | ||
| state="open", | ||
| per_page=100, | ||
| ).content | ||
| ) | ||
| except RequestFailed: | ||
| capture_exception() | ||
| raise RuntimeError("Request to list pull requests failed") from None | ||
|
StanFromIreland marked this conversation as resolved.
|
||
|
|
||
| for pull_request in pull_requests: | ||
| pull_request_author = pull_request["user"]["login"] | ||
| credit_if_uncredited(login=pull_request_author, type="remediation_developer") | ||
| try: | ||
| reviews = json.loads( | ||
| github.rest.pulls.list_reviews( | ||
| owner=private_fork_owner, | ||
| repo=private_fork_repo, | ||
| pull_number=pull_request["number"], | ||
| ).content | ||
| ) | ||
| except RequestFailed: | ||
| capture_exception() | ||
| raise RuntimeError("Request to list pull requests reviews failed") from None | ||
|
StanFromIreland marked this conversation as resolved.
|
||
|
|
||
| for review in reviews: | ||
| review_login = review["user"]["login"] | ||
| if review_login == pull_request_author: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, this is a little bit of an edge case, but if someone opens a PR, and then reviews an alternative PR by a different author, the credit type is dependant on the order in which the PRs are processed. Do we care? Not really, it's hard to judge which one will be more correct, I think we can manually fix it up if this occurs. |
||
| continue # Developers can't be reviewers too. | ||
| credit_if_uncredited( | ||
| login=review_login, | ||
| type="remediation_reviewer", | ||
|
sethmlarson marked this conversation as resolved.
|
||
| ) | ||
|
|
||
| return sort_security_advisory_credits(credits) | ||
|
|
||
|
|
||
| def github_client_request(client: typing.Any, method: str, url: str, params: dict[str, str | int]) -> typing.Any: | ||
| """Sends a raw HTTP request using a GitHub API client""" | ||
| headers = {"X-GitHub-Api-Version": client._REST_API_VERSION} | ||
|
|
@@ -119,6 +188,11 @@ def reserve_one_cve(cve_api: CveApi) -> str: | |
| return cve_ids[0] | ||
|
|
||
|
|
||
| def sort_security_advisory_credits(credits: list[dict[str, str]]) -> list[dict[str, str]]: | ||
| """Sorts the 'credits' field in a GitHub Security Advisory for comparison""" | ||
| return sorted(credits, key=lambda c: (c["login"], c["type"])) | ||
|
|
||
|
|
||
| def apply_to_repo(github: GitHub, owner: str, repo: str, cve_api: CveApi, *, reserve_cves: bool = True) -> None: | ||
| """Applies the PSRT GitHub Security Advisory process to the repository.""" | ||
| security_advisories = get_repository_advisories(github, owner, repo) | ||
|
|
@@ -184,6 +258,13 @@ def apply_to_repo(github: GitHub, owner: str, repo: str, cve_api: CveApi, *, res | |
| patch_data["collaborating_teams"] = sorted(collaborating_teams) | ||
| print(f" ➕ Will ensure team present: {PSRT_GITHUB_TEAM_SLUG}") | ||
|
|
||
| # Find new credits for the security advisory. | ||
| existing_credits = sort_security_advisory_credits(security_advisory.get("credits", None) or []) | ||
| new_credits = get_security_advisory_credits(github, security_advisory) | ||
| if new_credits and existing_credits != new_credits: | ||
| patch_data["credits"] = new_credits | ||
|
sethmlarson marked this conversation as resolved.
|
||
| print(" 📋 Will add credits for developing and reviewing remediation") | ||
|
|
||
| # Apply updates, if any, to the security advisory. | ||
| if patch_data: | ||
| try: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.