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
175 changes: 51 additions & 124 deletions plugins/magpie-security/skills/issue-deduplicate/SKILL.md

Large diffs are not rendered by default.

207 changes: 44 additions & 163 deletions plugins/magpie-security/skills/issue-deduplicate/merge-templates.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->

# security-issue-deduplicate — merged body and rollup-entry templates
# security-issue-deduplicate — merged body and rollup entries

`<sm>` is the `security-management issue-deduplicate` command of [`SKILL.md`](SKILL.md#prerequisites).

## Step 3 — Build the merged body proposal

Expand All @@ -10,182 +12,61 @@ The body-field schema (role names, empty-field convention, body-field-surgery pa
[`tools/github/issue-template.md`](../../../../tools/github/issue-template.md);
the adopting project's concrete field names live in
[`<project-config>/project.md`](../../../../<project-config>/project.md#issue-template-fields).
Structure:

```markdown
### The issue description

<keep.issue_description verbatim>

---

**Second independent report: [<tracker>#<drop>](https://github.com/<tracker>/issues/<drop>) — merged on <YYYY-MM-DD>.** <one-sentence headline: same root-cause bug, different attack vector / affected process.>

<details>
<summary>Full report from <drop.reporter> (click to expand)</summary>

<one-paragraph summary of WHY the two reports are the same root-cause
bug — same function, same file, same allowlist fix — but describe
different attack vectors / affected processes / threat-model
boundaries. This paragraph is the skill's own analysis, written
for a future triager who wants to understand why the two were
merged; write it so it reads naturally even after the duplicate
tracker has been closed for months.

<drop.issue_description verbatim>

</details>

### Short public summary for publish

<merged summary covering both vectors; if either side was `_No
response_`, use the populated side; if both were populated,
combine them with a leading sentence that covers both attack
vectors explicitly — the release manager will refine at Step 13>

### Affected versions

<widen the range to the broader of the two — take the lower `version
`-bound and the higher `lessThan` upper bound from both sides>

### Security mailing list thread

<keep.reporter> (<keep.context>): <keep's thread URL or Gmail threadId note>
<drop.reporter> (<drop.context>): <drop's thread URL or Gmail threadId note>
```
(one line per reporter; keep them in chronological order of the
original report, earliest first)

```markdown
### Public advisory URL

<keep's value; normally _No response_ at the time of merge>

### Reporter credited as

<keep.credit line verbatim>
<drop.credit line verbatim>
```
(one line per credit; preserve the *exact* form each reporter
confirmed, or the placeholder form when unconfirmed; the merge
does not silently re-synthesize credits)

**Apply the [bot/AI credit policy](../../../../tools/cve-tool-vulnogram/bot-credits-policy.md)
(at `tools/<cve-tool>/bot-credits-policy.md`) when consolidating.**
A **finder-side** credit line (*Reporter credited as*) on either tracker that matches the bot detection rule
(`*[bot]` suffix, known-bot list, `*-bot`/`*-ai`/`*-agent`/`*-gpt` / `*scanner*` / `*automat*` suffix patterns, automation-name list)
goes into the kept tracker's *Reporter credited as* field unchanged;
the CVE JSON generator emits it with `type: "tool"` per the policy's finder-side rule.
Surface it in the proposal as *"credited as tool (during merge): `<line>` (matches bot policy — `<rule>`)"*,
with the source tracker number, so the user sees which rows are routed as tools.
If the drop tracker has an inbound reporter thread to reply on, also propose the policy's *clarification-reply* Gmail draft
asking whether a human behind the bot/AI handle should be **additionally** credited as finder.
**What you write.** Two pieces of your own, and two merged values only when the trackers disagree:

- `headline` — one sentence for the visible line above the collapsed report: same root-cause bug, different attack vector or affected process.
- `analysis` — one paragraph of WHY the two reports are the same root-cause bug (same function, same file, same fix)
while describing different attack vectors, affected processes or threat-model boundaries.
It is written for a future triager, so make it read naturally months after the duplicate was closed.
- `merged_summary` — only when both *Short public summary* fields are populated and differ: one summary covering both vectors explicitly (the release manager refines it at Step 13).
- `affected_versions` — only when both *Affected versions* differ: the broader range, the lower `version` bound and the higher `lessThan` bound of the two.
- `cwe` — only after the user resolved a CWE conflict: the CWE they picked.

Write `{tracker, date, headline, analysis, keep, drop, merged_summary, affected_versions, cwe, body_fields}` — each side `{number, body, created}` from Step 1 — and run `<sm> merge-body`.
It rewrites the kept body one field at a time with github-body-field's `replace_field`,
which refuses any rewrite that would change another field, so the CVE JSON attachment, a project's extra fields and every unchanged value stay byte-exact:

- *The issue description*: the kept report as it is, then **Second independent report: `<drop link>` — merged on `<date>`.** with your headline,
over a collapsed `<details>` (*Full report from `<drop reporter>`*) holding your analysis and the dropped report **verbatim** —
except that reporter-supplied CVSS vectors, scores and severity labels are removed (`scores_removed`),
a line that would end a field or carries a machine marker gains a leading `\`, and an unmatched `</details>` is escaped so the disclosure cannot close early.
A chain merge (the dropped report already holding its own *Second independent report* block) stays nested, never flattened.
- *Reporter credited as*, *Security mailing list thread*, *PR with the fix*: both sides, one line each, verbatim — the merge never re-synthesises a credit; threads earliest report first.
- *Short public summary*, *Affected versions*: the one populated side, or your merged value; both populated and different without one is refused.
- *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).

**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,
and, when the dropped tracker has an inbound reporter thread, propose the policy's *clarification-reply* draft asking whether a human behind the handle should be **additionally** credited as finder.
A bot-matching *Remediation developer* on the dropped side is skipped (`skipped_credits`; the policy has no tool mapping there): surface it as *"skipped credit (during merge): `<line>`"*.
The user can override per the policy doc.

On the **remediation-developer side**, the original *skip* rule still applies:
a bot-matching line in either tracker's *Remediation developer* field is dropped from the merge result
(the policy has no `type: "tool"` mapping for remediation-developer credits).
Surface it as *"skipped credit (during merge): `<line>` (matches bot policy — `<rule>`)"*.

Manual credits a human security-team member typed in (visible in the issue timeline) are always preserved verbatim on both sides;
the filter only fires on credit lines auto-extracted upstream.

```markdown
### PR with the fix

<keep's value, or merge if both are populated>

### CWE

<the more specific of the two values; if they disagree on the
primary CWE, surface the disagreement as a blocker for the
triager rather than silently picking one>

### Severity

<keep's value; do NOT propagate a reporter-supplied CVSS from the
dropped tracker into the kept tracker's Severity field — the
independent-scoring rule in AGENTS.md applies to merged content
the same way it applies to a single reporter's content>

### CVE tool link

<keep's value>
```

The **Second independent report** block is the load-bearing part of the merge:
every future triager reads both reports in one place, without chasing the closed duplicate's content.
Inside the `<details>` disclosure, append the drop side's body **verbatim except for reporter-supplied
CVSS scores, CVSS vectors, and qualitative severity labels** — preserve the reporter's wording, code
blocks, and PoC text. Do not paraphrase.
The one-sentence headline that stays visible above the collapsed block is for scroll-readers
(the short-headline-over-collapsed-details pattern the status-change comments use),
so a long secondary report does not push every other body field below the fold.

If the drop-side body already had a *"Second independent report"* `<details>` block (chain-merge case — rare),
nest its content inside the new outer block (or append it as a sibling sub-block) so the chain of merges stays visible.
Never flatten or rewrite earlier merges.

## Step 4 — Build the rollup-entry proposals

Two rollup-comment entries, one per tracker — **not** two new top-level comments —
appended to each tracker's existing status-rollup comment (created by `security-issue-import`) via the upsert recipe in
appended to each tracker's existing status-rollup comment via the upsert recipe in
[`tools/github/status-rollup.md`](../../../../tools/github/status-rollup.md#upsert-recipe--append-to-an-existing-rollup-or-create-one).
When either tracker has no rollup yet (legacy tracker pre-dating the convention),
the recipe's Step 2b creates one and folds any pre-existing legacy bot comments in on the way.
the append creates one, and Step 5 folds any pre-existing legacy bot comments in first.

Draft only each entry's body; the Step 5 tool call writes the
Write `{tracker, keep, drop, cve_id, record_url_template, retract_cve, headline, analysis, next_step, notification, field_changes, blockers, artifacts}` —
each side `{number, credit, thread}` — and run `<sm> render-rollup-kept` and `<sm> render-rollup-dropped`.
Each prints the entry body and its action label (`Merge (kept) (from #<drop>)`, `Merge (dropped) (into #<keep>)`); the Step 5 call writes the
`<details><summary>YYYY-MM-DD · @user · <Action></summary>` envelope.
The action labels are `Merge (kept) (from #<drop>)` and
`Merge (dropped) (into #<keep>)`.
Start every body line at column 0 — leading spaces inside the
envelope render as a code block.

### Entry appended to the kept tracker's rollup

```markdown
**Merged [<tracker>#<drop>](https://github.com/<tracker>/issues/<drop>) into this tracker.** <one-sentence headline: same root-cause bug, different attack vector / affected process.>

- Body: <keep.reporter>'s original report preserved; <drop.reporter>'s report appended as *"Second independent report"*.
- Credits: **<keep credit>** + **<drop credit>**.
- Mailing threads: both listed.
- CVE: [<CVE-N>-<M>](<cve-record-url>) stays allocated here; [<tracker>#<drop>](https://github.com/<tracker>/issues/<N>) being closed as duplicate. The `<cve-record-url>` form is assembled from `cve_authority.record_url_template` in [`<project-config>/project.md`](../../../../<project-config>/project.md#cve-authority).

**Next:** <one-line next step — e.g. credit-preference confirmation for both, or Step 6 CVE refinement>.

<Reporter-notification line — one of the four canonical options from the sync skill.>

Full analysis of why the two reports are the same root-cause bug (same function, same file, same allowlist fix) but describe different attack vectors / affected processes / threat-model boundaries. Per-field hand-off details:

- *Reporter credited as*: <full before → after>.
- *Security mailing list thread*: <full before → after, including PonyMail URLs and Gmail thread IDs>.
- *Short public summary for publish*: <kept as-is | seeded with a merged draft starting "..."/>.
- *CWE*: <set to <value> | kept as _No response_ | BLOCKER: conflict between <keep.cwe> and <drop.cwe> — triager to resolve>.
- *Affected versions*: widened to <value>.
- CVE JSON attachment regenerated: <comment URL>.
```

### Entry appended to the dropped tracker's rollup

```markdown
**Closing as duplicate of [<tracker>#<keep>](https://github.com/<tracker>/issues/<keep>).** <one-sentence headline.>

Full content merged into [<tracker>#<keep>](https://github.com/<tracker>/issues/<N>) as *"Second independent report"*; <drop.reporter> credited alongside <keep.reporter> there.

All triage and advisory work continues on [#<keep>](https://github.com/<tracker>/issues/<N>).

<one-paragraph analysis matching the kept-side details>.

Specific artifacts merged: <CVSS scoring, attack chain, PoC, remediation options, etc.>.

See [the merge entry on <tracker>#<keep>](https://github.com/<tracker>/issues/<N>) for the full hand-off record.

<Reporter-notification line — one of the four canonical options from the sync skill.>
```
- The kept entry links the dropped tracker, lists both credits and both threads, the CVE that stays allocated here (and the dropped side's being retracted),
the **Next:** step, the *Reporter notification* line, your analysis, the per-field hand-off (`field_changes`: *field* and its before → after) and every blocker.
- The dropped entry links the kept tracker, says the content was merged there as *"Second independent report"* with both reporters credited,
points all further work at the kept tracker, and carries your analysis and the merged artifacts (CVSS scoring, attack chain, PoC, remediation options).

Both entries must render every cross-issue reference as a clickable markdown link, per
[`AGENTS.md` § *Linking tracker issues and PRs*](../../../../AGENTS.md#linking-tracker-issues-and-prs).
There is no six-line visible cap: the entire entry is already collapsed inside `<details>`,
so write what the auditor needs. Do not pad.
The *Reporter notification* line is one of the four canonical options of the sync skill (`notification`: `notified`, `team_member`, `draft_pending` with a draft ID checked to be still in Drafts, or `omitted`).
Every line starts at column 0 and every cross-tracker reference is a link; `bare_refs` must be empty.
There is no six-line visible cap: the entire entry is already collapsed inside `<details>`, so write what the auditor needs. Do not pad.

---
Loading