diff --git a/skills/security-issue-import/SKILL.md b/skills/security-issue-import/SKILL.md index 9ca69f9f6..11c8c4a78 100644 --- a/skills/security-issue-import/SKILL.md +++ b/skills/security-issue-import/SKILL.md @@ -1190,7 +1190,7 @@ here. | **Affected versions** | Extract the version(s) / range (`` / `>= X, < Y` / `= 2.6.0, < 2.10.2`. **Do not prefix the product name** (the tracker is already project-scoped, so ` 2.9.0` is redundant — record `2.9.0`). If the reporter gave only a single version they tested on (e.g. `3.1.5`), record that verbatim; the triager can widen the range later. Leave `_No response_` if no version is mentioned. | | **Security mailing list thread** | **Keep the private thread handle, and — if possible — also link the PonyMail archive entry.** The full URL-construction recipe (search URL template, month-token format, user-pastes-back flow, Gmail-threadId fallback) lives in [`tools/gmail/ponymail-archive.md`](../../tools/gmail/ponymail-archive.md#use-case--security-issue-import); the adopting project's private-search URL template is declared in [`/project.md`](../..//project.md#gmail-and-ponymail). Propose the constructed search URL to the user at Step 5, wait for them to paste back the resolved `/thread/?` URL, and record the PonyMail URL, the Gmail `threadId`, **and the inbound report's root `Message-ID`** in this field. The root `Message-ID` is the archive-independent handle for the message (a Gmail `threadId` resolves only inside the one mailbox that holds it; the `Message-ID` is what the reporter's MUA stamped and what PonyMail hashes its permalinks on), so it keeps the report locatable even from an account that never received the Gmail copy. Resolve it per backend per [`tools/gmail/operations.md` — Get the root `Message-ID` of a thread](../../tools/gmail/operations.md#get-the-root-message-id-of-a-thread) (PonyMail results carry it directly; on the Gmail backend the claude.ai MCP does **not** expose it, so use the `oauth-draft-message-id` helper). Record it on its own line as ``Root Message-ID: `` `` — **backtick-wrap it**, since a bare `<...@...>` renders as an HTML tag on GitHub. The whole field is **internal-only** — the `generate-cve-json` script will not export it to `references[]` — see the "CVE references must never point at non-public mailing-list threads" section of [`AGENTS.md`](../../AGENTS.md). | | **Public advisory URL** | `_No response_`. Populated at Step 14 by `security-issue-sync` once the advisory is archived. | -| **Reporter credited as** | The reporter's full display name from the email `From:` header (e.g. `Alice Example` from `"Alice Example" `). **When the body carries an explicit attribution line** — e.g. `Credit: discovered and reported by of `, common in ASF-security-relay forwards where the `From:` is `` and the sender header is only a routing artefact — that line is **authoritative**: record the credited party **as written, including any affiliation** (e.g. `Jordan Lee of Horizon Security Research`, not just `Jordan Lee`). This is a **placeholder** — in direct-reporter mode, the receipt-of-confirmation reply in Step 7 asks the reporter to confirm their preferred credit form. **Apply the [bot/AI credit policy](../../tools/cve-tool-vulnogram/bot-credits-policy.md) before populating** — if the `From:`-header name or address matches the bot detection rule (`*[bot]` suffix, known-bot list, `*-bot`/`*-ai`/`*-agent`/`*-gpt` suffix patterns, `noreply`/`no-reply`/`donotreply` / `security-alerts@` / `notifications@` service sender), **include** the detected name in the field (the CVE JSON generator emits it with `type: "tool"` per the policy's finder-side rule) and surface *"credited as tool: `` (matches bot policy — ``)"* in Step 5's proposal. Service-sender addresses (noreply / relays) are still suppressed from the field — they are routing artefacts, not identities; extract the real reporter from the email body instead. **In direct-reporter mode**, also fold the policy's *clarification-reply* into the Step 7 receipt-of-confirmation draft, asking whether a human behind the bot/AI handle should be **additionally** credited as finder (the tool credit stands either way). **In via-forwarder mode** (when the optional [`security-issue-import-via-forwarder`](../security-issue-import-via-forwarder/SKILL.md) sub-skill pre-classified the candidate via a registered forwarder adapter and the other cases enumerated in [`docs/security/forwarder-routing-policy.md`](../../docs/security/forwarder-routing-policy.md#when-does-via-forwarder-mode-apply)), the **standalone** bot-credit clarification draft is suppressed — it is a credit-acceptance confirmation message, which the forwarder cannot meaningfully answer. The credit *question* itself is **not** suppressed: it folds as a single best-effort *"if a human was behind the tool, please pass back their preferred attribution"* line into the Step 7 receipt-of-confirmation draft instead, per the [question-vs-confirmation distinction](../../docs/security/forwarder-routing-policy.md#negative-space--do-not-relay) in the forwarder-routing policy. The same bot-detection rule applies to the forwarder adapter's `extract_credit()` output (the detection runs on the relayed credit string, not on the forwarder's sender address); see [`tools/forwarder-relay/README.md`](../../tools/forwarder-relay/README.md) for the adapter contract. The user can override per the policy doc. | +| **Reporter credited as** | The reporter's full display name from the email `From:` header (e.g. `Alice Example` from `"Alice Example" `). **When the body carries an explicit attribution line** — e.g. `Credit: discovered and reported by of `, common in ASF-security-relay forwards where the `From:` is `` and the sender header is only a routing artefact — that line is **authoritative**: record the credited party **as written, including any affiliation** (e.g. `Jordan Lee of Horizon Security Research`, not just `Jordan Lee`). This is a **placeholder** — in direct-reporter mode, the receipt-of-confirmation reply in Step 7 asks the reporter to confirm their preferred credit form. **Apply the [bot/AI credit policy](../../tools/cve-tool-vulnogram/bot-credits-policy.md) before populating** — if the `From:`-header name or address matches the bot detection rule (`*[bot]` suffix, known-bot list, `*-bot`/`*-ai`/`*-agent`/`*-gpt` suffix patterns, `noreply`/`no-reply`/`donotreply` / `security-alerts@` / `notifications@` service sender), **include** the detected name in the field (the CVE JSON generator emits it with `type: "tool"` per the policy's finder-side rule) and surface *"credited as tool: `` (matches bot policy — ``)"* in Step 5's proposal. Service-sender addresses (noreply / relays) are still suppressed from the field — they are routing artefacts, not identities; extract the real reporter from the email body instead. **In direct-reporter mode**, also fold the policy's *clarification-reply* into the Step 7 receipt-of-confirmation draft, asking whether a human behind the bot/AI handle should be **additionally** credited as finder (the tool credit stands either way). **In via-forwarder mode** (when the optional [`security-issue-import-via-forwarder`](../security-issue-import-via-forwarder/SKILL.md) sub-skill pre-classified the candidate via a registered forwarder adapter and the other cases enumerated in [`docs/security/forwarder-routing-policy.md`](../../docs/security/forwarder-routing-policy.md#when-does-via-forwarder-mode-apply)), the **standalone** bot-credit clarification draft is suppressed — it is a credit-acceptance confirmation message, which the forwarder cannot meaningfully answer. The credit *question* itself is **not** suppressed: it folds as a single best-effort *"if a human was behind the tool, please pass back their preferred attribution"* line into the Step 7 receipt-of-confirmation draft instead, per the [question-vs-confirmation distinction](../../docs/security/forwarder-routing-policy.md#negative-space--do-not-relay) in the forwarder-routing policy. The same bot-detection rule applies to the forwarder adapter's `extract_credit()` output (the detection runs on the relayed credit string, not on the forwarder's sender address); see [`tools/forwarder-relay/README.md`](../../tools/forwarder-relay/README.md) for the adapter contract. The user can override per the policy doc. **Whether the report earns a `finder` credit at all is a separate question** — apply the [finder-credit policy](../../tools/cve-tool-vulnogram/finder-credit-policy.md) as well: a report that arrived after a public fix PR was already *opened* earns no finder credit (Rule 1), and where there is no finder to name the field is left empty rather than set to `anonymous` (Rule 2). | | **PR with the fix** | `_No response_`. | | **Remediation developer** | `_No response_`. Auto-populated by the `security-issue-sync` skill from the linked PR's author the first time *PR with the fix* is set; manual edits are preserved on subsequent syncs. The auto-populate step applies the same [bot/AI credit policy](../../tools/cve-tool-vulnogram/bot-credits-policy.md). | | **CWE** | `_No response_`. The security team scores CWE independently; a reporter-supplied CWE is informational only (per the *"Reporter-supplied CVSS scores are informational only"* rule in [`AGENTS.md`](../../AGENTS.md)). Do **not** copy a CWE from the reporter's body into this field. | diff --git a/skills/security-issue-sync/gather.md b/skills/security-issue-sync/gather.md index 3664a9cc7..7b01230c5 100644 --- a/skills/security-issue-sync/gather.md +++ b/skills/security-issue-sync/gather.md @@ -264,6 +264,13 @@ Process for finding the real reporter and the original thread: known-bot list, `*-bot`/`*-ai`/`*-agent`/`*-gpt` suffix patterns), propose landing the credit anyway — the CVE JSON generator will emit it with `type: "tool"` per the policy's finder-side rule. + + **Then apply the [finder-credit policy](../../tools/cve-tool-vulnogram/finder-credit-policy.md)**, + which decides whether the credit is earned at all: a report that + arrived after a public fix PR was already *opened* earns no + `finder` credit (Rule 1, with its four exceptions), and where + there is no finder to name the field is left empty rather than + set to `anonymous` (Rule 2). Surface in Step 2 *"credited as tool: `` (matches bot policy — ``)"* **and propose a Gmail draft on the reporter's thread** per the policy's *clarification-reply* diff --git a/tools/cve-tool-vulnogram/bot-credits-policy.md b/tools/cve-tool-vulnogram/bot-credits-policy.md index 73e5b2f41..0c9b73e00 100644 --- a/tools/cve-tool-vulnogram/bot-credits-policy.md +++ b/tools/cve-tool-vulnogram/bot-credits-policy.md @@ -42,6 +42,11 @@ This file is the single source of truth for *who counts as a bot* and *how the skills + the CVE JSON generator behave when one is detected*. Both reference it instead of duplicating the heuristic. +This file answers *what category a credited party belongs to*. +Whether a given report earns a `finder` credit **at all** — the +timing gate, and how to represent the absence of a finder — is +[`finder-credit-policy.md`](finder-credit-policy.md). + ## Why * Bot accounts (Dependabot, Renovate, GHSA Probot, GitHub Actions, diff --git a/tools/cve-tool-vulnogram/finder-credit-policy.md b/tools/cve-tool-vulnogram/finder-credit-policy.md new file mode 100644 index 000000000..8c1a76bb6 --- /dev/null +++ b/tools/cve-tool-vulnogram/finder-credit-policy.md @@ -0,0 +1,160 @@ + + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Finder-credit policy — when a report earns a `finder` credit](#finder-credit-policy--when-a-report-earns-a-finder-credit) + - [Rule 1 — a report earns no credit when a public fix PR was already open](#rule-1--a-report-earns-no-credit-when-a-public-fix-pr-was-already-open) + - [How to apply](#how-to-apply) + - [Exceptions](#exceptions) + - [Rule 2 — no placeholder credit: omit rather than write `anonymous`](#rule-2--no-placeholder-credit-omit-rather-than-write-anonymous) + - [What the reporter is told](#what-the-reporter-is-told) + - [Where the rule applies](#where-the-rule-applies) + - [Worked example](#worked-example) + + + + + +# Finder-credit policy — when a report earns a `finder` credit + +The CVE 5.x `credits[]` schema records who discovered a +vulnerability (`type: "finder"`). Deciding *whether a given report +earns that credit at all* is a separate question from deciding +*what category* a credited party belongs to — the latter is +[`bot-credits-policy.md`](bot-credits-policy.md), which routes bots +and AI accounts to `type: "tool"`. + +This file is the single source of truth for the former. Two rules: +one about **timing**, one about **the absence of a finder**. + +## Rule 1 — a report earns no credit when a public fix PR was already open + +**A report earns no finder credit when a public fix PR for the same +issue was already *opened* in `` before the report +arrived.** + +The bar is **PR opened**, not PR merged. An open pull request is +public: its diff, its title, and the code path it touches are +readable by anyone. A report that arrives afterwards is not telling +the project something it did not already know and had not already +begun to fix. + +### How to apply + +Two timestamps decide it, both from authoritative sources — never +from the reporter's or the tracker's prose: + +| Value | Source | +|---|---| +| PR opened | `gh pr view --repo --json createdAt` | +| Report arrived | the inbound message's `Date:` header, or the advisory's submission timestamp | + +Report arrived **after** PR opened → **no credit**. Record the +decision and both timestamps in the tracker's status rollup, so the +call is auditable later without re-deriving it. + +### Exceptions + +- **The original finder.** A reporter whose report *caused* the PR + is credited, even though the PR predates every later duplicate. + Their report necessarily predates it. +- **A genuinely different prong.** A later report that identifies a + sink, reach path, or bypass the open PR does **not** address is a + new finding, not a duplicate — credit applies on its own merits. + Decide this on the fix scope, not on the reporter's framing: a + report that merely describes the same bug in more detail (a + sharper exploitation path, a better PoC, an additional trigger for + the same sink) is still the same finding and earns no credit. +- **A credit already offered in writing.** An offer of credit, once + made to a reporter, is honoured — even when a later timestamp + check would have refused it. **Apply this gate *before* offering + credit, never after.** Withdrawing an offer a reporter has read, + and possibly accepted, costs the project far more than one credit + line, and the reporter did nothing to deserve it. If an offer was + made in error, the error is the project's to absorb. +- **A credit already published.** Once an advisory has shipped, + credit is part of the public record; correcting it is the + credit-correction flow, not a silent retraction. Before + publication, credit is freely revisable — subject to the + written-offer rule above. + +## Rule 2 — no placeholder credit: omit rather than write `anonymous` + +When there is no named finder to credit — an internal audit +finding, a scanner-sourced report with no human reporter, or a +reporter who declined attribution — **omit the `finder` credit +entirely**. Do **not** write a placeholder `finder` credit of +`anonymous`, and do not substitute the project's own security team +as a stand-in for "nobody external". + +An empty *Reporter credited as* field makes the CVE-JSON generator +emit no `finder` entry, which is the correct record: a `credits[]` +carrying only the `remediation developer` is normal and complete. A +literal `anonymous` finder adds noise without information — it +occupies the space where a reader looks for attribution and answers +nothing. + +- Set *Reporter credited as* to the empty-field marker, not + `anonymous`, when there is no external finder to name. +- This is orthogonal to Rule 1: the timing gate decides *whether a + named reporter earns credit*; this rule decides *how to represent + the absence of one*. +- **This rule applies only when there is genuinely no finder to + name.** It is not a licence to leave the field empty because + nobody answered the credit question. Establish that no preference + was supplied **anywhere in the reporter thread, including the + original report body**, before treating the absence as real. +- Where a **tool** discovered the finding, Rule 2 does not mean + "credit nobody": the tool itself is credited with `type: "tool"` + per [`bot-credits-policy.md`](bot-credits-policy.md). Rule 2 + governs only the `finder` row. +- The **remediation developer** credit is unaffected — it names who + wrote the fix and is always populated. + +## What the reporter is told + +Non-credit is not a brush-off, and the reply says so: thank the +reporter, name the PR, and ask them to verify it covers what they +observed — inviting them back on the thread if it does not. The +*fix-already-public* reply shape in +[`security-issue-import`](../../skills/security-issue-import/SKILL.md) +carries this; substitute *"opened on"* when the PR has not merged. + +## Where the rule applies + +- [`security-issue-import`](../../skills/security-issue-import/SKILL.md) + — the `fix-already-public` class, and any candidate whose + existing-PR search finds an open PR. +- [`security-issue-deduplicate`](../../skills/security-issue-deduplicate/SKILL.md) + — before folding a later report's credit into the kept tracker. +- [`security-cve-allocate`](../../skills/security-cve-allocate/SKILL.md) + — before writing `credits[]`. +- [`security-issue-sync`](../../skills/security-issue-sync/SKILL.md) + — when reconciling a tracker's credit field against the CVE + record. + +## Worked example + +A fix PR opens on **2026-06-13** in response to the first report, +and stays open for weeks. Three reports describe the same sink: + +| Reporter | Reported | Credit | +|---|---|---| +| First reporter | 2026-06-06 | **Yes** — original finder; the PR was written in response | +| Second reporter | 2026-07-07 | **No** — 24 days after the PR opened | +| Third reporter | 2026-07-25 | **No** — 42 days after the PR opened | + +All three describe the same sink the PR restricts, so neither of the +later two qualifies under the different-prong exception. + +**The failure mode this prevents.** Without an explicit rule, the +second reporter is added as a co-finder on arrival, because the PR +has not merged yet and "not merged" reads as "not yet public". The +credit then has to be retracted once the rule is drawn — which is +exactly the withdrawal the *credit already offered in writing* +exception exists to prevent. Apply the gate at intake, before any +credit is offered, and the retraction never becomes necessary.