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
2 changes: 1 addition & 1 deletion skills/security-issue-import/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,7 @@ here.
| **Affected versions** | Extract the version(s) / range (`<version>` / `>= X, < Y` / `<Y`) the reporter states and record them as **bare, comma-separated version numbers** — e.g. `2.9.0, 2.9.3` or `>= 2.6.0, < 2.10.2`. **Do not prefix the product name** (the tracker is already project-scoped, so `<product> 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-config>/project.md`](../../<project-config>/project.md#gmail-and-ponymail). Propose the constructed search URL to the user at Step 5, wait for them to paste back the resolved `<mail-archive-url>/thread/<hash>?<security-list>` 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: `<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" <alice@example.com>`). **When the body carries an explicit attribution line** — e.g. `Credit: discovered and reported by <name> of <org>`, common in ASF-security-relay forwards where the `From:` is `<security-list>` 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: `<name>` (matches bot policy — `<rule>`)"* 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" <alice@example.com>`). **When the body carries an explicit attribution line** — e.g. `Credit: discovered and reported by <name> of <org>`, common in ASF-security-relay forwards where the `From:` is `<security-list>` 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: `<name>` (matches bot policy — `<rule>`)"* 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. |
Expand Down
7 changes: 7 additions & 0 deletions skills/security-issue-sync/gather.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<handle>` (matches bot
policy — `<which rule fired>`)"* **and propose a Gmail draft on
the reporter's thread** per the policy's *clarification-reply*
Expand Down
5 changes: 5 additions & 0 deletions tools/cve-tool-vulnogram/bot-credits-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
160 changes: 160 additions & 0 deletions tools/cve-tool-vulnogram/finder-credit-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->

# 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 `<upstream>` 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 <N> --repo <upstream> --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.