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
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ def ref(value: object) -> str:
if 7 in chosen:
if not retract or not cve_id:
return {"ok": False, "refused": "item 7 retracts the dropped side's CVE and needs both CVE IDs"}
if retract == cve_id:
return {"ok": False, "refused": f"item 7 would retract {cve_id}, the kept tracker's CVE"}
steps.append(
{
"item": 7,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def link(side: Mapping[str, Any]) -> str:
f"tracker's {keep_cve} ({states['keep']}): keep the drop tracker instead.",
}
)
else:
elif drop_cve != keep_cve:
retract = drop_cve
if probe and not found:
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ def ref(value: object) -> str:
if 7 in chosen:
if not retract or not cve_id:
return {"ok": False, "refused": "item 7 retracts the dropped side's CVE and needs both CVE IDs"}
if retract == cve_id:
return {"ok": False, "refused": f"item 7 would retract {cve_id}, the kept tracker's CVE"}
steps.append(
{
"item": 7,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def link(side: Mapping[str, Any]) -> str:
f"tracker's {keep_cve} ({states['keep']}): keep the drop tracker instead.",
}
)
else:
elif drop_cve != keep_cve:
retract = drop_cve
if probe and not found:
return {
Expand Down
20 changes: 20 additions & 0 deletions tools/security-management/tests/test_issue_deduplicate.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ def test_case_4_already_duplicate(capsys, tmp_path):
None,
"CVE-2025-2222",
),
(
side(42, ["airflow"], cve_link=C1, cve_state="allocated"),
side(67, ["airflow"], cve_link=C1, cve_state="allocated"),
"proceed",
None,
None,
),
(
side(42, ["airflow"], cve_link=C1, cve_state="allocated"),
side(67, ["airflow"], cve_link=C2, cve_state="publish-ready"),
Expand Down Expand Up @@ -602,6 +609,19 @@ def test_case_2_with_blockers(capsys, tmp_path):
assert "**Still to resolve:**" in out["text"]


def test_apply_plan_refuses_to_retract_the_kept_cve(capsys, tmp_path):
data = {
"tracker": TRACKER,
"keep": {"number": 42},
"drop": {"number": 67},
"scratch": "/s",
"items": [7],
"cve_id": "CVE-2025-1111",
"retract_cve": "CVE-2025-1111",
}
assert run(capsys, tmp_path, "apply-plan", data)[0] == 2


def test_apply_plan_refuses_a_malformed_cve_id(capsys, tmp_path):
base = {
"tracker": TRACKER,
Expand Down
3 changes: 2 additions & 1 deletion tools/spec-loop/specs/security-issue-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ publication, with a human gate and an audit-log entry at every step.
are open with the same single scope label and neither `duplicate`,
applies the CVE rules (a CVE only on the dropped side means swap, a
`public` record refuses, the kept CVE must be at least as far along and
the dropped one is retracted), reads both bodies' fields (the CVE JSON
the dropped one is retracted, unless both trackers carry the same CVE),
reads both bodies' fields (the CVE JSON
attachment cut off), and rewrites the kept body field by field through
github-body-field's `replace_field`, so the attachment and extra fields
stay byte-exact: the dropped report verbatim in a collapsed *Second
Expand Down
Loading