Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion blurb_it/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,19 @@ async def handle_add_blurb_post(request: Request) -> Response:
issue_number = data.get("issue_number", "").strip()
section = data.get("section", "").strip()
news_entry = data.get("news_entry", "").strip() + "\n"
cve = data.get("cve", "").strip()
if cve:
if section != util.CVE_SECTION:
raise web.HTTPBadRequest(
reason=f"A CVE may only be given for the {util.CVE_SECTION} section."
)
cve = util.normalize_cve(cve)
if cve is None:
raise web.HTTPBadRequest(
reason="Invalid CVE identifier (expected CVE-YYYY-NNNN)."
)
path = await util.get_misc_news_filename(issue_number, section, news_entry)
news_content = util.get_misc_news_content(news_entry, cve=cve)
pr_number = data.get("pr_number", "").strip()

context = {}
Expand Down Expand Up @@ -190,7 +202,7 @@ async def handle_add_blurb_post(request: Request) -> Response:
)
pr = await gh.getitem(f"/repos/python/cpython/pulls/{pr_number}")
pr_repo_full_name = pr["head"]["repo"]["full_name"]
encoded = base64.b64encode(str.encode(news_entry))
encoded = base64.b64encode(str.encode(news_content))
decoded = encoded.decode("utf-8")
put_data = {
"branch": pr["head"]["ref"],
Expand Down
22 changes: 22 additions & 0 deletions blurb_it/util.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
import base64
import hashlib
import os
import re
import time
import secrets

from aiohttp_session import get_session

from blurb_it import error

# A CVE may only be attached to a Security entry, and is stored as a ".. cve:"
# metadata line at the top of the news file.
CVE_SECTION = "Security"
_cve_re = re.compile(r"(?:CVE-)?(\d{4})-(\d{4,})", re.IGNORECASE | re.ASCII)


def normalize_cve(value):
"""Return a CVE identifier in canonical "CVE-YYYY-NNNN" form."""
match = _cve_re.fullmatch(value.strip())
if match is None:
return None
year, number = match.groups()
return f"CVE-{year}-{number}"


def get_misc_news_content(body, cve=None):
"""Return the news file contents, with ".. cve:" metadata when given."""
if not cve:
return body
return f".. cve: {cve}\n\n{body}"


async def get_misc_news_filename(issue_number, section, body):
if " " in section:
Expand Down
8 changes: 8 additions & 0 deletions templates/add_blurb.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ <h3>📜🤖 Blurb it again?</h3>
</select>
</div>
</div>
<div class="mb-3">
<div class="col form-inline">
<label for="cve" class="form-inline">CVE</label><input type="text" class="form-control form-inline" pattern="^(?:[Cc][Vv][Ee]-)?\d{4}-\d{4,}$" maxlength="20" placeholder="CVE-2025-12345" id="cve" name="cve">
</div>
<div class="col form-inline">
<small id="cve-help" class="form-text text-muted">Optional. The CVE identifier (CVE-YYYY-NNNN) for a <strong>Security</strong> entry only.</small>
</div>
</div>
<div class="mb-3">
<textarea class="form-control" id="news_entry" name="news_entry" rows="10" placeholder="Write your Misc/NEWS entry below.
It should be a simple ReST paragraph.
Expand Down
28 changes: 28 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,31 @@ async def test_get_installation_not_found():
with pytest.raises(error.InstallationNotFound) as exc:
await util.get_installation(gh, "fake_jwt", "octonauts")
assert exc.value.args[0] == "Can't find installation by that user: octonauts"


@pytest.mark.parametrize(
"value, expected",
[
("CVE-2025-12345", "CVE-2025-12345"),
("cve-2025-12345", "CVE-2025-12345"),
("2025-12345", "CVE-2025-12345"),
(" CVE-2025-1234 ", "CVE-2025-1234"),
("CVE-2025-123", None),
("CVE-25-12345", None),
("CVE-2025-12345 extra", None),
("gh-12345", None),
("", None),
],
)
def test_normalize_cve(value, expected):
assert util.normalize_cve(value) == expected


def test_get_misc_news_content_without_cve():
assert util.get_misc_news_content("Fix a thing.\n") == "Fix a thing.\n"
assert util.get_misc_news_content("Fix a thing.\n", cve=None) == "Fix a thing.\n"


def test_get_misc_news_content_with_cve():
content = util.get_misc_news_content("Fix a thing.\n", cve="CVE-2025-12345")
assert content == ".. cve: CVE-2025-12345\n\nFix a thing.\n"
Loading