From 6ccae73936c74aaf5b1ea3f372c9cede56955b76 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sat, 29 Aug 2026 21:35:50 +0200 Subject: [PATCH] fix(security-issue-sync): omit the finder credit, never fall back to anonymous MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconciles #1124 with #1127, as promised in #1127's description. #1124 made `anonymous` the fallback for a scanner-sourced finding whose scanner declares no public credit name. #1127 then landed the finder-credit policy, whose Rule 2 says the opposite: where there is no named finder, omit the `finder` credit rather than writing a placeholder. Both are on `main`, so the framework currently contradicts itself in three places. Rule 2 wins. It is grounded in an ASF CVE reviewer's feedback on a published batch — "I'm not sure how helpful it is to credit 'anonymous' as finder, just leave it out" — and a `credits[]` carrying only the remediation developer is a complete record, whereas a literal `anonymous` occupies the space a reader scans for attribution and answers nothing. Nothing else about #1124 changes: the scanner's public credit name is still written when declared, still emitted with `type: "tool"`, and the confidential product token still never reaches a public surface. Only the no-name-declared branch changes, from writing a placeholder to omitting the row. Grep confirms no `anonymous` fallback remains under skills/, projects/, or tools/. Generated-by: Claude Code (Claude Opus 5) --- projects/_template/scanner-products.md | 7 ++++--- skills/security-issue-sync/apply-and-push.md | 6 ++++-- skills/security-issue-sync/gather.md | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/projects/_template/scanner-products.md b/projects/_template/scanner-products.md index f4d2529d..c9897c7e 100644 --- a/projects/_template/scanner-products.md +++ b/projects/_template/scanner-products.md @@ -78,9 +78,10 @@ above: the scanner-product name from the *Short public summary for publish* body field text. Suppressing the un-consenting **individual** is the goal; discarding the - discovery attribution entirely is not. Fall back to - `anonymous` only when the scanner declares no public - credit name. + discovery attribution entirely is not. When the scanner + declares no public credit name, **omit the `finder` credit** + rather than writing a placeholder — see [Rule 2 of the + finder-credit policy](../../tools/cve-tool-vulnogram/finder-credit-policy.md). 2. **Audit trail stays**: the *Security mailing list thread* field, the status-rollup comment, and the Gmail / PonyMail thread keep the original scanner-product + person-name diff --git a/skills/security-issue-sync/apply-and-push.md b/skills/security-issue-sync/apply-and-push.md index 541375f2..63daca9d 100644 --- a/skills/security-issue-sync/apply-and-push.md +++ b/skills/security-issue-sync/apply-and-push.md @@ -626,8 +626,10 @@ Step 6 below describes how to verify the state advance landed `discovery-channel` signal is private. On match: propose replacing the credit value with the scanner's declared **public credit name**, emitted with `type: "tool"` per - [`bot-credits-policy.md`](../../tools/cve-tool-vulnogram/bot-credits-policy.md) — falling back - to `anonymous` only when none is declared — and stripping + [`bot-credits-policy.md`](../../tools/cve-tool-vulnogram/bot-credits-policy.md). When none is + declared, **omit the `finder` credit** rather than writing a + placeholder, per [Rule 2 of the finder-credit + policy](../../tools/cve-tool-vulnogram/finder-credit-policy.md). Also strip the scanner product name from the summary text. The tracker's *Security mailing list thread* body field stays unchanged (it is the private audit trail); only the diff --git a/skills/security-issue-sync/gather.md b/skills/security-issue-sync/gather.md index 6ebc7d1e..333cb322 100644 --- a/skills/security-issue-sync/gather.md +++ b/skills/security-issue-sync/gather.md @@ -374,7 +374,7 @@ update, label change, or next-step recommendation in Step 2: | The *"Short public summary for publish"* body field is populated but does **not** state the triggering conditions — the rendered text describes the bug mechanism without identifying (a) the attacker role / capability, (b) the deployment configuration that has to be active, OR (c) the action the attacker takes against which surface. Detector heuristic: scan the summary for any of these phrases — *"an authenticated [\\w ]+ user"*, *"a Dag author"*, *"an attacker with"*, *"a user able to"*, *"when [\\w]+ is (configured\|enabled\|set)"*, *"affects deployments where"*, *"by [verb-ing]"*, *"who [verb-s]"*. If fewer than two of the three (who / when / action) are unambiguously present, the summary fails the trigger-conditions check. | Propose expanding the summary to add the missing condition(s) per the *triggering conditions* requirement in Step 2b — `who` (attacker role / required capability), `when` (deployment shape / config / feature that has to be active), `action` (the step taken against which surface). **Why:** the reader scans the published advisory asking *"does this affect us?"*, and the answer comes from the trigger context, not the bug mechanism. A summary that omits one of the three forces them to read the issue PR / patch to figure out the trigger — exactly the work the advisory is meant to remove. Apply this on every sync run that surfaces a trigger-incomplete summary, even when no other body update is being proposed for the tracker. After updating, regenerate the CVE JSON attachment so the published `descriptions[].value` reflects the trigger context. | | The tracker is an **incomplete-fix follow-up to another CVE** — detected by any of: the rollup or body mentions *"incomplete fix for `CVE-YYYY-NNNNN`"* / *"follow-up to `CVE-YYYY-NNNNN`"* / *"sibling tracker"*; the title contains a *"(incomplete fix for `CVE-YYYY-NNNNN`)"* parenthetical; the `affected[]` array names a different `packageName` than the referenced prior CVE; OR the tracker was opened as a split from a closed-`announced` tracker whose CVE is already PUBLISHED — **AND** the *Short public summary for publish* body field does not yet contain BOTH (a) the prior `CVE-YYYY-NNNNN` ID verbatim AND (b) a *"users who already applied [the prior CVE's fix] should also apply this one"* clause naming the current product/package. | Propose expanding the summary to add the cross-CVE + cross-product upgrade ask per the *"Incomplete-fix-to-another-CVE"* paragraph in Step 2b. Concretely, the summary must (1) name the prior CVE explicitly, (2) state that the prior fix did not cover the current product/surface, (3) tell users who already applied the prior fix to **also** apply this one (the two are complementary, not duplicates). **Why:** when a CVE is published as a follow-up to a prior CVE, the reader's default reading is *"I already applied the earlier fix; this is a duplicate."* Without explicit cross-CVE + cross-product framing in the summary, downstream consumers miss that two upgrades are needed. Apply this fix on every sync run that surfaces an incomplete-fix tracker whose summary lacks the cross-CVE clause, even when no other body update is being proposed. After updating, regenerate the CVE JSON attachment so the published `descriptions[].value` reflects the cross-CVE relationship. | | The *"CWE"* body field is populated with a bare `CWE-NNN` token (no description text) — e.g. `CWE-22` or `CWE-502` alone, without the canonical short description that follows in the format `CWE-NNN: ` | Propose expanding the field to `CWE-NNN: <Canonical Title>` per the MITRE CWE catalog (e.g. `CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')`, `CWE-502: Deserialization of Untrusted Data`, `CWE-601: URL Redirection to Untrusted Site ('Open Redirect')`). **Prefer a CWE from the project's *advised CWEs* list** when one is declared in [`<project-config>/scope-labels.md`](../../<project-config>/scope-labels.md) or the project's CVE-tool config — the advised list captures the CWE classes the project's security team has standardised on, and using one from the list makes cross-CVE comparison cleaner. **Why:** the published CVE record's `problemTypes[].descriptions[].description` field carries the human-readable text the advisory mailing list and `cve.org` render; a bare `CWE-NNN` is technically a valid identifier but useless to readers who don't keep the MITRE numbering in their head. The longer form costs nothing to add and significantly improves the published advisory's clarity. Apply on every sync run that surfaces a bare CWE token. After updating, regenerate the CVE JSON attachment so `problemTypes[]` carries the expanded form. | -| The tracker's *Security mailing list thread* body field references a **private scanner product** (declared in [`<project-config>/scanner-products.md`](../../<project-config>/scanner-products.md) — e.g. internal SAST, partner-shared scan, unpublished bug-bounty pipeline) **AND** the *Reporter credited as* body field names a person rather than `anonymous` / a public handle, **AND** there is no signal the finder consented to public credit (no inbound `security@` message from them under their own name, no public HackerOne / huntr.dev report URL on the thread, no explicit *"please credit me as `<name>`"* line). | Propose rewriting the *Reporter credited as* field to the scanner's **public credit name** (the `Public credit name` column in [`<project-config>/scanner-products.md`](../../<project-config>/scanner-products.md)), which the CVE-JSON generator emits with `type: "tool"` per [`bot-credits-policy.md`](../../tools/cve-tool-vulnogram/bot-credits-policy.md) — falling back to `anonymous` only when the scanner declares no public credit name — and stripping the scanner product name from the *Short public summary for publish* body field text (e.g. *"Mythos scan flagged that…"* → drop the scanner-product clause; *"Imported from internal SAST"* → drop). **Audit-trail surfaces stay untouched**: the *Security mailing list thread* body field, the status-rollup comment, and the Gmail thread keep the original scanner-product + person-name references for security-team auditing. Only the CVE-record-bound surfaces (summary, credit) get the anonymise scrub. **Why:** scanner-tool product names are commercial / IP-sensitive (naming the scanner publicly amounts to free advertising and some contracts restrict attribution), and individual finders sourced from private channels haven't consented to public credit (their org pointed a scanner at the codebase and shared findings privately — there was no `security@` thread asking to be named). The *combination* of (named individual + named proprietary scanner) also pattern-leaks the discovery channel and how that org runs security on its codebase. Apply on every sync run that surfaces the signal; the *Reporter credited as* field is read verbatim by the CVE-JSON generator into `credits[]` and the *Short public summary for publish* is read into `descriptions[].value`. The same scrub re-runs as the sixth pre-push hygiene gate in [Step 5b 1b](apply-and-push.md#decision-flow) — it catches the case where the body update was missed at proposal time and the JSON would otherwise ship with the scanner name still in it. **Exempt cases**: when the finder already self-credited under a public name (HackerOne report URL, huntr.dev public report URL, the reporter's own `security@` message naming themselves), keep the named credit — the scrubber must not anonymise a credit that was already public elsewhere. | +| The tracker's *Security mailing list thread* body field references a **private scanner product** (declared in [`<project-config>/scanner-products.md`](../../<project-config>/scanner-products.md) — e.g. internal SAST, partner-shared scan, unpublished bug-bounty pipeline) **AND** the *Reporter credited as* body field names a person rather than `anonymous` / a public handle, **AND** there is no signal the finder consented to public credit (no inbound `security@` message from them under their own name, no public HackerOne / huntr.dev report URL on the thread, no explicit *"please credit me as `<name>`"* line). | Propose rewriting the *Reporter credited as* field to the scanner's **public credit name** (the `Public credit name` column in [`<project-config>/scanner-products.md`](../../<project-config>/scanner-products.md)), which the CVE-JSON generator emits with `type: "tool"` per [`bot-credits-policy.md`](../../tools/cve-tool-vulnogram/bot-credits-policy.md) — and, when the scanner declares no public credit name, **omitting the `finder` credit** rather than writing a placeholder, per [Rule 2 of the finder-credit policy](../../tools/cve-tool-vulnogram/finder-credit-policy.md) — and stripping the scanner product name from the *Short public summary for publish* body field text (e.g. *"Mythos scan flagged that…"* → drop the scanner-product clause; *"Imported from internal SAST"* → drop). **Audit-trail surfaces stay untouched**: the *Security mailing list thread* body field, the status-rollup comment, and the Gmail thread keep the original scanner-product + person-name references for security-team auditing. Only the CVE-record-bound surfaces (summary, credit) get the anonymise scrub. **Why:** scanner-tool product names are commercial / IP-sensitive (naming the scanner publicly amounts to free advertising and some contracts restrict attribution), and individual finders sourced from private channels haven't consented to public credit (their org pointed a scanner at the codebase and shared findings privately — there was no `security@` thread asking to be named). The *combination* of (named individual + named proprietary scanner) also pattern-leaks the discovery channel and how that org runs security on its codebase. Apply on every sync run that surfaces the signal; the *Reporter credited as* field is read verbatim by the CVE-JSON generator into `credits[]` and the *Short public summary for publish* is read into `descriptions[].value`. The same scrub re-runs as the sixth pre-push hygiene gate in [Step 5b 1b](apply-and-push.md#decision-flow) — it catches the case where the body update was missed at proposal time and the JSON would otherwise ship with the scanner name still in it. **Exempt cases**: when the finder already self-credited under a public name (HackerOne report URL, huntr.dev public report URL, the reporter's own `security@` message naming themselves), keep the named credit — the scrubber must not anonymise a credit that was already public elsewhere. | | The **issue title** contains adopter-specific or internal noise that would otherwise ship to the public CVE record — leading or trailing project-name tokens (e.g. ``<vendor> <product>:`` / ``in <vendor> <product>`` / ``(<vendor> <product> X.Y)``), internal split markers (``(split from #NNN)`` / ``(split for scope clarity from #NNN)``), report-form classifiers (``[ Security Report ]`` / ``[Security Issue]``), external-tracker IDs in parentheses or brackets (``[GHSA-xxxx-xxxx-xxxx]``, ``(ZDRES-NNNNN)``, ``(HUNTR-NNNNN)``, ``(GHSL-NNNN-NNN)``), version-noise suffixes (``(v3.2.1)``, ``(3.x)``), prior-CVE-relationship parentheticals anywhere in the title (``(CVE-YYYY-NNNNN)`` / ``(possible CVE-YYYY-NNNNN variant)`` / ``(incomplete fix for CVE-YYYY-NNNNN)`` / ``(fix-bypass of CVE-YYYY-NNNNN)`` — the cross-CVE relationship belongs in the public summary's Gate #3 clause, never in the title), or trailing reporter-name attribution parentheticals (``(Evan Ricafort follow-up)`` / ``(<name> follow-up)`` — reporter attribution belongs in the credits field, never in the public title). The check applies on every sync pass, including trackers whose title was previously clean but has drifted since allocation. | Propose updating the title via `gh issue edit <N> --title "<cleaned>"`. **Reuse the [`security-cve-allocate` Step 2 title-strip cascade](../security-cve-allocate/SKILL.md#step-2--compute-the-cve-ready-title)** — both the leading-pattern set (project-name tokens, `Security (Report\|Issue\|Vulnerability\|Bug)` prefixes) and the trailing-pattern set (`in (<vendor> )?<product>`, GHSA/ZDRES/HUNTR/GHSL trailing IDs, `(split from #N)` parentheticals). **Why this matters even though `security-cve-allocate` already strips at allocation time:** the GitHub issue title is read **verbatim** by the CVE-JSON generator into `containers.cna.title`, which ships in the published advisory and on `cve.org`. Titles drift between allocation and the final regen (manual edits to add context, sibling-tracker splits, GHSA-relay imports that append the GHSA ID), so the sync skill must re-run the same cleanup on every pass. **Preserve stripped context as audit trail** in the issue body (a `### Related references` section near the bottom) or in the rollup — internal pointers like *"split from [#NNN](https://github.com/<tracker>/issues/<N>)"* are useful for the security team and must not be silently lost; just move them off the user-facing title. After updating the title, regenerate the CVE JSON attachment so the published `title` field reflects the cleaned value. If the strip would collapse the title to fewer than 3 words, **flag the ambiguity** in the proposal (matching `security-cve-allocate`'s safety) and let the user override — over-stripping is worse than leaving one redundant word. | | A release carrying the fix has shipped. Detection is **scope-dependent** — different scope labels on a project can ride different release trains, each with its own *"is it released?"* signal (which artifact registry to consult, what to query, how to map a tracker's milestone to that registry, partial-release edge cases). The per-scope detection recipe lives in [`<project-config>/scope-labels.md` — *Detecting that a fix release has shipped*](../../<project-config>/scope-labels.md#detecting-that-a-fix-release-has-shipped). The "or an explicit *fix shipped in X.Y.Z* comment" fallback applies across all scopes regardless of the project-specific signal. **Read the release signal from an authoritative source:** the project's release **announcement** (mailing-list `[ANNOUNCE]` / registry release feed) and, where available, the package **changelog naming the fix PR** and/or the **git-ancestry of the fix commit under the release tag** (e.g. a `compare <release-tag>...<merge-commit>` that reports the commit as an ancestor). **Do not infer release state from a prose-summarizing fetch of a large package-index JSON API** (an LLM `WebFetch`-style summary can silently mangle the version / upload-date and report a *false "not released"*, which strands a shippable advisory at `pr merged` with no RM hand-off); use a structured read or the authoritative announcement instead. A *false negative* here is the more dangerous direction — it withholds a hand-off — so when the signals disagree, trust the announcement + changelog over any summarized fetch. | **Two-stage gate: every mandatory CVE field must be populated AND the CVE record state in Vulnogram must be `REVIEW`.** Before proposing either the label swap or the assignee swap, run both checks. **Stage 1 — body fields**: check that all six body fields are populated (not empty, not `_No response_`): *CWE*, *Affected versions*, *Severity*, *Reporter credited as*, *Short public summary for publish*, *PR with the fix*. If any is missing, **do NOT propose the hand-off**. Instead, propose posting (or PATCH-updating) the *Remediation-developer fill-fields comment* per the dedicated bullet in Step 2b — issue stays assigned to the remediation developer; no label swap, no assignee swap, no RM hand-off. **Stage 2 — CVE state**: with Stage 1 clear, Step 5b's `vulnogram-api-record-update` push includes `body.CNA_private.state = "REVIEW"` (the new auto-promote behaviour — see Step 5b for details). After the push, verify the record state is now `REVIEW` (via `vulnogram-api-record-fetch` / the equivalent state probe). If the state is still `DRAFT` after the push (push failed, CNA-schema validation rejected the JSON, transient error), **re-fire the fill-fields comment** with the refreshed blocker description, and **do NOT propose the hand-off / label swap / assignee swap on this pass**. The RM never receives a hand-off while the record is in `DRAFT`. **When both stages are clear (state == REVIEW)**: propose swapping `pr merged` → `fix released` (Step 12). This is the release manager's cue to own Steps 13–15 (advisory send → URL capture → Vulnogram PUBLIC → close). **Also propose swapping the assignee from the remediation developer to the release manager** (looked up via the three-source cascade in Step 2c — [`<project-config>/release-trains.md`](../../<project-config>/release-trains.md) "Release managers for releases currently relevant to the security tracker" → Release Plan wiki → `[RESULT][VOTE]` thread on `dev@`), so the issue list reflects ownership hand-off. See the *Assignee hand-off at the `fix released` transition* paragraph under **Assignees** in Step 2b for the full rule. | | GHSA state transition (opened, accepted, published, rejected) in a GHSA-forwarded email | If the GHSA is closed as "not accepted" but the security team accepted the report on `security@`, flag the divergence in the status comment so it is not lost. |