From 01279921e24ef3176a9e09d00112195a87f7abb9 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 2 Apr 2026 10:31:55 +0200 Subject: [PATCH 1/5] Automatically close advisories that have `[CLOSED]` in the title --- src/psrt_ghsa_bot/app.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/psrt_ghsa_bot/app.py b/src/psrt_ghsa_bot/app.py index 60da65b..4a8df43 100644 --- a/src/psrt_ghsa_bot/app.py +++ b/src/psrt_ghsa_bot/app.py @@ -101,6 +101,18 @@ def apply_to_repo( print(f" 📋 Processing {ghsa_id} (state: {state})") + # If the summary starts with "[CLOSED]", close the advisory. + summary = security_advisory.get("summary", "") + if summary.upper().startswith("[CLOSED]"): + github.rest.security_advisories.update_repository_advisory( + owner=owner, + repo=repo, + ghsa_id=ghsa_id, + data={"state": "closed"}, + ) + print(f" 📋 Closed {ghsa_id}") + continue + # Maintain a dictionary of updates to make and then submit them all at once. patch_data = {} From 6cda7998e82e2b2ee49baddeafdc195282123294 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Thu, 2 Apr 2026 10:01:02 -0500 Subject: [PATCH 2/5] Accept [CLOSE] or [CLOSED] in any position --- src/psrt_ghsa_bot/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/psrt_ghsa_bot/app.py b/src/psrt_ghsa_bot/app.py index 4a8df43..e9b1181 100644 --- a/src/psrt_ghsa_bot/app.py +++ b/src/psrt_ghsa_bot/app.py @@ -101,9 +101,10 @@ def apply_to_repo( print(f" 📋 Processing {ghsa_id} (state: {state})") - # If the summary starts with "[CLOSED]", close the advisory. + # If the summary contains '[CLOSE]' or [CLOSED]' then + # we can close the ticket. summary = security_advisory.get("summary", "") - if summary.upper().startswith("[CLOSED]"): + if re.search(r"\[CLOSED?\]", summary.upper()) is not None: github.rest.security_advisories.update_repository_advisory( owner=owner, repo=repo, From 947212884fe67bde632a797d66941d31e5bfe94d Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 2 Apr 2026 19:21:05 +0200 Subject: [PATCH 3/5] Allow accepting reports, and add tests --- src/psrt_ghsa_bot/app.py | 14 ++++++++--- tests/test_app.py | 54 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/psrt_ghsa_bot/app.py b/src/psrt_ghsa_bot/app.py index e9b1181..ead6bee 100644 --- a/src/psrt_ghsa_bot/app.py +++ b/src/psrt_ghsa_bot/app.py @@ -4,6 +4,7 @@ import csv import datetime import os +import re import typing import urllib3 @@ -101,10 +102,10 @@ def apply_to_repo( print(f" 📋 Processing {ghsa_id} (state: {state})") - # If the summary contains '[CLOSE]' or [CLOSED]' then - # we can close the ticket. + # If the summary contains '[CLOSE]', '[CLOSED]', '[COMPLETE]', + # or '[COMPLETED]' then we can close the ticket. summary = security_advisory.get("summary", "") - if re.search(r"\[CLOSED?\]", summary.upper()) is not None: + if re.search(r"\[(?:CLOSED?|COMPLETED?)\]", summary.upper()) is not None: github.rest.security_advisories.update_repository_advisory( owner=owner, repo=repo, @@ -117,9 +118,14 @@ def apply_to_repo( # Maintain a dictionary of updates to make and then submit them all at once. patch_data = {} + # If the summary contains '[ACCEPT]' or '[DRAFT]', transition it to the draft state. + if state == "triage" and re.search(r"\[(?:ACCEPT(?:ED)?|DRAFT)\]", summary.upper()) is not None: + patch_data["state"] = "draft" + print(f" 📋 Moving {ghsa_id} to drafts") + # Advisories that are in the 'draft' state without a CVE ID # should have one allocated by the PSF CVE Numbering Authority. - if state == "draft" and security_advisory.get("cve_id") is None: + if (state == "draft" or patch_data.get("state") == "draft") and security_advisory.get("cve_id") is None: cve_id = reserve_one_cve(cve_api) patch_data["cve_id"] = cve_id print(f" ✅ Will reserve CVE ID: {cve_id}") diff --git a/tests/test_app.py b/tests/test_app.py index ac06c49..a01d9ba 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -34,11 +34,12 @@ def cve_reserve_response(cve_id, year): } -def _create_advisory_dict(state, cve_id, collaborating_teams): +def _create_advisory_dict(state, cve_id, collaborating_teams, summary=""): """Helper to create a security advisory dictionary.""" return { "ghsa_id": "GHSA-xxxx-xxxx-xxxx", "state": state, + "summary": summary, "cve_id": cve_id, "collaborating_teams": [{"slug": team} for team in collaborating_teams], "collaborating_users": [{"login": "octocat", "id": 1, "type": "User"}], @@ -177,6 +178,57 @@ def test_update_collaborating_users() -> None: ) +@pytest.mark.parametrize("summary", [ + "[CLOSE] perl is better than Python", + "[CLOSED] 0.1 + 0.2 is broken?!?!?!?!?!", + "[COMPLETE] some boring security thing", + "fix soemthing in datetime module [COMPLETED]", + "blah blah [closed] lowercase blah"] +) +def test_closes_advisory_with_close_or_complete_tag(summary) -> None: + security_advisory = _create_advisory_dict("triage", None, [], summary=summary) + + github = mock.Mock() + cve_api = mock.Mock() + + with mock.patch("psrt_ghsa_bot.app.get_repository_advisories") as get_repo_advs: + get_repo_advs.return_value = [security_advisory] + + app.apply_to_repo(github, "owner", "repo", cve_api) + + github.rest.security_advisories.update_repository_advisory.assert_called_once_with( + owner="owner", + repo="repo", + ghsa_id="GHSA-xxxx-xxxx-xxxx", + data={"state": "closed"}, + ) + + +@pytest.mark.parametrize("summary", [ + "[ACCEPT] critical vulnerability in str.lowercase()", + "spam foo bar [ACCEPTED]", + "i'm bored of these [draft]",] +) +def test_drafts_triage_advisory_with_accept_or_draft_tag(summary, cve_id, cve_reserve_response) -> None: + security_advisory = _create_advisory_dict("triage", None, [], summary=summary) + + github = mock.Mock() + cve_api = mock.Mock() + cve_api.reserve.return_value = cve_reserve_response + + with mock.patch("psrt_ghsa_bot.app.get_repository_advisories") as get_repo_advs: + get_repo_advs.return_value = [security_advisory] + + app.apply_to_repo(github, "owner", "repo", cve_api) + + github.rest.security_advisories.update_repository_advisory.assert_called_once_with( + owner="owner", + repo="repo", + ghsa_id="GHSA-xxxx-xxxx-xxxx", + data={"state": "draft", "cve_id": cve_id, "collaborating_teams": ["psrt"]}, + ) + + def test_load_psrt_members_from_devguide() -> None: with mock.patch("psrt_ghsa_bot.app.urllib3.request") as urllib3_request: resp = mock.Mock() From ec35f89441e1e25423e82fd507a85d16d9aef303 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 2 Apr 2026 19:24:06 +0200 Subject: [PATCH 4/5] ruff --- tests/test_app.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index a01d9ba..9db58f3 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -178,12 +178,15 @@ def test_update_collaborating_users() -> None: ) -@pytest.mark.parametrize("summary", [ - "[CLOSE] perl is better than Python", - "[CLOSED] 0.1 + 0.2 is broken?!?!?!?!?!", - "[COMPLETE] some boring security thing", - "fix soemthing in datetime module [COMPLETED]", - "blah blah [closed] lowercase blah"] +@pytest.mark.parametrize( + "summary", + [ + "[CLOSE] perl is better than Python", + "[CLOSED] 0.1 + 0.2 is broken?!?!?!?!?!", + "[COMPLETE] some boring security thing", + "fix soemthing in datetime module [COMPLETED]", + "blah blah [closed] lowercase blah", + ], ) def test_closes_advisory_with_close_or_complete_tag(summary) -> None: security_advisory = _create_advisory_dict("triage", None, [], summary=summary) @@ -204,10 +207,13 @@ def test_closes_advisory_with_close_or_complete_tag(summary) -> None: ) -@pytest.mark.parametrize("summary", [ - "[ACCEPT] critical vulnerability in str.lowercase()", - "spam foo bar [ACCEPTED]", - "i'm bored of these [draft]",] +@pytest.mark.parametrize( + "summary", + [ + "[ACCEPT] critical vulnerability in str.lowercase()", + "spam foo bar [ACCEPTED]", + "i'm bored of these [draft]", + ], ) def test_drafts_triage_advisory_with_accept_or_draft_tag(summary, cve_id, cve_reserve_response) -> None: security_advisory = _create_advisory_dict("triage", None, [], summary=summary) From 5ca3f3e6787438c11b5cf0dd778b7d467dd911c5 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Thu, 2 Apr 2026 19:56:05 +0200 Subject: [PATCH 5/5] Remove accept bit --- src/psrt_ghsa_bot/app.py | 7 +------ tests/test_app.py | 28 ---------------------------- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/src/psrt_ghsa_bot/app.py b/src/psrt_ghsa_bot/app.py index ead6bee..851d5e1 100644 --- a/src/psrt_ghsa_bot/app.py +++ b/src/psrt_ghsa_bot/app.py @@ -118,14 +118,9 @@ def apply_to_repo( # Maintain a dictionary of updates to make and then submit them all at once. patch_data = {} - # If the summary contains '[ACCEPT]' or '[DRAFT]', transition it to the draft state. - if state == "triage" and re.search(r"\[(?:ACCEPT(?:ED)?|DRAFT)\]", summary.upper()) is not None: - patch_data["state"] = "draft" - print(f" 📋 Moving {ghsa_id} to drafts") - # Advisories that are in the 'draft' state without a CVE ID # should have one allocated by the PSF CVE Numbering Authority. - if (state == "draft" or patch_data.get("state") == "draft") and security_advisory.get("cve_id") is None: + if state == "draft" and security_advisory.get("cve_id") is None: cve_id = reserve_one_cve(cve_api) patch_data["cve_id"] = cve_id print(f" ✅ Will reserve CVE ID: {cve_id}") diff --git a/tests/test_app.py b/tests/test_app.py index 9db58f3..bb1d124 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -207,34 +207,6 @@ def test_closes_advisory_with_close_or_complete_tag(summary) -> None: ) -@pytest.mark.parametrize( - "summary", - [ - "[ACCEPT] critical vulnerability in str.lowercase()", - "spam foo bar [ACCEPTED]", - "i'm bored of these [draft]", - ], -) -def test_drafts_triage_advisory_with_accept_or_draft_tag(summary, cve_id, cve_reserve_response) -> None: - security_advisory = _create_advisory_dict("triage", None, [], summary=summary) - - github = mock.Mock() - cve_api = mock.Mock() - cve_api.reserve.return_value = cve_reserve_response - - with mock.patch("psrt_ghsa_bot.app.get_repository_advisories") as get_repo_advs: - get_repo_advs.return_value = [security_advisory] - - app.apply_to_repo(github, "owner", "repo", cve_api) - - github.rest.security_advisories.update_repository_advisory.assert_called_once_with( - owner="owner", - repo="repo", - ghsa_id="GHSA-xxxx-xxxx-xxxx", - data={"state": "draft", "cve_id": cve_id, "collaborating_teams": ["psrt"]}, - ) - - def test_load_psrt_members_from_devguide() -> None: with mock.patch("psrt_ghsa_bot.app.urllib3.request") as urllib3_request: resp = mock.Mock()