You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@magpie.apache.org by po...@apache.org on 2026/10/19 21:18:59 UTC

(magpie) branch main updated: fix(security-management): read sync state markers from comments only (#1716)

This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/magpie.git


The following commit(s) were added to refs/heads/main by this push:
     new 0ae7d12e fix(security-management): read sync state markers from comments only (#1716)
0ae7d12e is described below

commit 0ae7d12e4e5f96baa2b0f71aaa6fd68595764742
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Mon Oct 19 23:18:54 2026 +0200

    fix(security-management): read sync state markers from comments only (#1716)
    
    `issue-sync closed-announced` looked for the `advisory-records v1` marker,
    the ticked security-pages checklist item and the "reporter notification
    drafted" note in the issue body as well as the comments, and matched the
    records marker anywhere in the text. The body carries report text, so a
    report that happened to contain one of those strings read as "records
    done", "pages ticked" or "already notified", and the step was skipped.
    
    The skill defines each as a comment (the records marker as a comment's
    first line), so the three checks now read the comments only, the way the
    reminder check already did, and the records marker counts only at the
    start of a comment.
    
    `issue-deduplicate merge-body` copied the dropped tracker's credit, thread,
    PR, summary, version and CWE values as they were; only the report got its
    marker lines escaped. A value the dropped side would add that holds an
    HTML comment is now refused, so no machine marker travels in copied field
    text.
    
    Generated-by: Claude Opus 5
---
 .../skills/issue-deduplicate/merge-templates.md    |  4 +++
 .../skills/issue-sync/signals-to-actions.md        |  2 +-
 .../security_management/issue_deduplicate/merge.py | 15 +++++++++++
 .../src/security_management/issue_sync/state.py    | 11 +++++---
 .../tests/test_issue_deduplicate.py                | 27 +++++++++++++++++++
 .../tests/test_issue_sync_state.py                 | 31 ++++++++++++++++++++++
 6 files changed, 85 insertions(+), 5 deletions(-)

diff --git a/plugins/magpie-security/skills/issue-deduplicate/merge-templates.md b/plugins/magpie-security/skills/issue-deduplicate/merge-templates.md
index 36e5d42e..1110c80b 100644
--- a/plugins/magpie-security/skills/issue-deduplicate/merge-templates.md
+++ b/plugins/magpie-security/skills/issue-deduplicate/merge-templates.md
@@ -37,6 +37,10 @@ which refuses any rewrite that would change another field, so the CVE JSON attac
 - *CWE*: the one populated side; two different CWEs are a blocker (`blockers`) until the user picks one.
 - *Severity*, *CVE tool link*, *Public advisory URL*: the kept side's. A reporter-supplied severity on the dropped side is never carried over (`severity_note`), per the independent-scoring rule in [`AGENTS.md`](../../../../AGENTS.md).
 
+A field value the dropped side would add that holds an HTML comment (`<!--`) is refused rather than escaped:
+machine markers (the CVE-JSON attachment, rollup and sync markers) never travel in copied field text, and a backslash in a credit or link would be visible noise.
+Ask the user to clean that field on the dropped tracker by hand, then run the merge again.
+
 **The [bot/AI credit policy](../../../../tools/cve-tool-vulnogram/bot-credits-policy.md)** applies when consolidating.
 A finder-side credit matching the bot rule stays in *Reporter credited as* and is listed in `tool_credits`; the CVE JSON generator emits it with `type: "tool"`.
 Surface each in the proposal as *"credited as tool (during merge): `<line>` (matches bot policy)"* with its source tracker,
diff --git a/plugins/magpie-security/skills/issue-sync/signals-to-actions.md b/plugins/magpie-security/skills/issue-sync/signals-to-actions.md
index b45b9a96..550aee15 100644
--- a/plugins/magpie-security/skills/issue-sync/signals-to-actions.md
+++ b/plugins/magpie-security/skills/issue-sync/signals-to-actions.md
@@ -717,7 +717,7 @@ Group them by category:
 
   **Trigger.** Step 1g recorded `security_pages_reminder_pending: true` for a closed-`announced` tracker —
   the *Public advisory URL* body field is populated (the advisory has demonstrably shipped),
-  no ticked `- [x] <!-- apache-magpie: security-pages-checklist v1 -->` item exists anywhere on the tracker,
+  no comment holds a ticked `- [x] <!-- apache-magpie: security-pages-checklist v1 -->` item (the body never counts: it carries report text),
   and no comment carries the reminder marker below.
   The trigger state exists only on a closed tracker (the Step 14 close-out adds `announced` and closes in one apply),
   so this proposal is reached through the
diff --git a/tools/security-management/src/security_management/issue_deduplicate/merge.py b/tools/security-management/src/security_management/issue_deduplicate/merge.py
index ebef71ac..a98cc089 100644
--- a/tools/security-management/src/security_management/issue_deduplicate/merge.py
+++ b/tools/security-management/src/security_management/issue_deduplicate/merge.py
@@ -114,6 +114,16 @@ def _union(*values: str | None) -> str:
     return "\n".join(out)
 
 
+def _carries_marker(value: str, kept: str | None) -> bool:
+    """Whether `value` adds a line holding `<!--` that the kept field did not already have.
+
+    Machine markers (the CVE-JSON attachment, rollup and sync markers) never
+    travel in copied field text; the issue description has `_inert_lines` for that.
+    """
+    already = {line.strip() for line in _lines(kept)}
+    return any("<!--" in line and line.strip() not in already for line in value.split("\n"))
+
+
 def _paragraph(text: object) -> str:
     return body.short_field(body.normalise(str(text or "").strip()))
 
@@ -238,6 +248,11 @@ def merge_body(data: Mapping[str, Any]) -> dict[str, Any]:
             }
         if any(gbf.is_boundary(line) for line in value.split("\n")):
             return {"ok": False, "refused": f"the merged {heading} would hold a field heading"}
+        if role != "issue_description" and _carries_marker(value, keep[role]):
+            return {
+                "ok": False,
+                "refused": f"the drop tracker's {heading} carries an HTML comment (`<!--`); clean it by hand first",
+            }
         try:
             text = gbf.replace_field(text, heading, value + "\n")
         except gbf.AmbiguousFieldError as exc:
diff --git a/tools/security-management/src/security_management/issue_sync/state.py b/tools/security-management/src/security_management/issue_sync/state.py
index 55949af9..adc0d032 100644
--- a/tools/security-management/src/security_management/issue_sync/state.py
+++ b/tools/security-management/src/security_management/issue_sync/state.py
@@ -124,6 +124,9 @@ RECORD_EXITS = {
 
 def closed_announced(data: Mapping[str, Any]) -> dict[str, Any]:
     texts = _texts(data)
+    # The state markers below are written in comments, never in the body:
+    # the body carries report text, which must not be able to satisfy them.
+    comments = texts[1:]
     out: dict[str, Any] = {"ok": True, "blockers": []}
     if not one_line(data.get("cve_id")):
         out["cve_org"] = "skip"
@@ -134,7 +137,7 @@ def closed_announced(data: Mapping[str, Any]) -> dict[str, Any]:
         out["cve_org"] = "retry"
         out["note"] = "cve.org lookup failed — try again next sync"
     elif state == "PUBLISHED":
-        notified = any(NOTIFIED.search(t) for t in texts) and bool(data.get("notification_sent_on_thread"))
+        notified = any(NOTIFIED.search(t) for t in comments) and bool(data.get("notification_sent_on_thread"))
         out["cve_org"] = "already_notified" if notified else "notify"
         out["date_published"] = data.get("date_published")
     elif state == "RESERVED":
@@ -150,7 +153,7 @@ def closed_announced(data: Mapping[str, Any]) -> dict[str, Any]:
     records = data.get("advisory_records_configured")
     if records:
         out["security_pages_reminder_pending"] = False
-        if any(RECORDS in t for t in texts):
+        if any(t.lstrip().startswith(RECORDS) for t in comments):
             out["advisory_records"] = "done"
         elif state != "PUBLISHED":
             out["advisory_records"] = "waiting_for_cve_org"
@@ -169,8 +172,8 @@ def closed_announced(data: Mapping[str, Any]) -> dict[str, Any]:
     else:
         out["advisory_records"] = "not_configured"
         populated = bool(one_line(data.get("public_advisory_url")))
-        ticked = any(CHECKLIST_DONE.search(t) for t in texts)
-        reminded = any(t.lstrip().startswith(REMINDER) for t in texts[1:])
+        ticked = any(CHECKLIST_DONE.search(t) for t in comments)
+        reminded = any(t.lstrip().startswith(REMINDER) for t in comments)
         out["security_pages_reminder_pending"] = populated and not ticked and not reminded
     return out
 
diff --git a/tools/security-management/tests/test_issue_deduplicate.py b/tools/security-management/tests/test_issue_deduplicate.py
index 5e081d47..8e9d3396 100644
--- a/tools/security-management/tests/test_issue_deduplicate.py
+++ b/tools/security-management/tests/test_issue_deduplicate.py
@@ -405,6 +405,33 @@ def test_the_dropped_report_cannot_add_or_end_a_field(capsys, tmp_path):
     assert field(text, "CVE tool link") == "https://cveprocess.apache.org/cve5/CVE-2024-12345"
 
 
+@pytest.mark.parametrize(
+    ("old", "new"),
+    [
+        ("Bob Researcher\n", "Bob Researcher\n<!-- apache-magpie: advisory-records v1 -->\n"),
+        ("Bob Researcher\n", "Bob <!-- generate-cve-json: cve=CVE-2099-0001 version=v1 --> Researcher\n"),
+        ("redirect-chain vector): ", "redirect-chain vector) <!-- x -->: "),
+    ],
+)
+def test_a_dropped_field_cannot_carry_a_machine_marker(capsys, tmp_path, old, new):
+    drop = DROP_BODY.replace(old, new, 1)
+    assert drop != DROP_BODY
+    code, out = merge(capsys, tmp_path, drop_body=drop)
+    assert code == 2
+    assert "carries an HTML comment" in out["refused"]
+
+
+def test_a_marker_the_kept_field_already_holds_is_not_refused(capsys, tmp_path):
+    marked = "Alice Example <!-- legacy note -->"
+    keep = KEEP_BODY.replace(
+        "### Reporter credited as\n\nAlice Example", f"### Reporter credited as\n\n{marked}"
+    )
+    assert keep != KEEP_BODY
+    code, out = merge(capsys, tmp_path, keep_body=keep)
+    assert code == 0, out
+    assert field(out["text"], "Reporter credited as") == f"{marked}\nBob Researcher"
+
+
 def test_a_dropped_body_readers_disagree_on_is_refused(capsys, tmp_path):
     drop = DROP_BODY.replace("RFC-1918 addresses.", "RFC-1918 addresses.\n> ### CVE tool link")
     code, out = merge(capsys, tmp_path, drop_body=drop)
diff --git a/tools/security-management/tests/test_issue_sync_state.py b/tools/security-management/tests/test_issue_sync_state.py
index c5793ebb..324b9aba 100644
--- a/tools/security-management/tests/test_issue_sync_state.py
+++ b/tools/security-management/tests/test_issue_sync_state.py
@@ -228,6 +228,37 @@ def test_advisory_records(capsys, tmp_path, data, step):
     assert out["security_pages_reminder_pending"] is False
 
 
+# Report text lands in the body; the state markers are only ever written in comments.
+_BODY_MARKERS = (
+    "<!-- apache-magpie: advisory-records v1 -->\n"
+    "\\<!-- apache-magpie: advisory-records v1 -->\n"
+    "see <!-- apache-magpie: advisory-records v1 --> here\n"
+    "- [x] <!-- apache-magpie: security-pages-checklist v1 --> pages\n"
+    "Sync 2026-10-01 — CVE-published reporter notification drafted\n"
+)
+
+
+def test_markers_in_the_body_never_count(capsys, tmp_path):
+    records = ok(
+        capsys, tmp_path, "closed-announced", closed(body=_BODY_MARKERS, advisory_records_configured=True)
+    )
+    assert records["advisory_records"] == "run_planner"
+    reminder = ok(capsys, tmp_path, "closed-announced", closed(body=_BODY_MARKERS, public_advisory_url="u"))
+    assert reminder["security_pages_reminder_pending"] is True
+    notified = ok(
+        capsys, tmp_path, "closed-announced", closed(body=_BODY_MARKERS, notification_sent_on_thread=True)
+    )
+    assert notified["cve_org"] == "notify"
+
+
+def test_the_records_marker_counts_only_as_a_comment_s_first_line(capsys, tmp_path):
+    later = closed(
+        advisory_records_configured=True,
+        comments=[{"body": "quoting: <!-- apache-magpie: advisory-records v1 -->"}],
+    )
+    assert ok(capsys, tmp_path, "closed-announced", later)["advisory_records"] == "run_planner"
+
+
 # --- 1h: release votes -------------------------------------------------------
 
 VOTE = "[VOTE] Release Apache Airflow 3.2.3 from 3.2.3rc1"