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/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new 7735bcf feat(security): skip obvious bot/AI accounts from CVE credits
by default (#276)
7735bcf is described below
commit 7735bcf1f01c06c082ae2bdac15604e3ac3fcc28
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon May 25 13:26:55 2026 +0200
feat(security): skip obvious bot/AI accounts from CVE credits by default
(#276)
Adds a bot/AI credit policy that fires at every site where the security
skill suite auto-extracts a candidate finder or remediation-developer
credit. When the candidate matches the policy (obvious bot or
automation), the skills now skip the credit by default, surface the
skip with the matched rule so the user can judge, and -- on email-
backed trackers -- propose a Gmail draft asking the reporter whether
the bot/AI handle is the intended attribution or whether a human
behind it should be credited instead.
- New `tools/vulnogram/bot-credits-policy.md`: single source of truth
for the detection heuristic and per-skill enforcement contract.
Detection covers GitHub `[bot]` suffix; known-bot / automation-name
list (dependabot, renovate, snyk, copilot, github-actions, mend,
mergify, automated, scanner, sast/dast, ...); suffix patterns
(`*-bot`, `*-ai`, `*-agent`, `*-gpt`, `*scanner*`, `*automat*`);
and noreply / service-relay email senders. Default behaviour: skip
silently in the data flow + surface the skip with which rule fired
+ honour explicit per-tracker overrides + propose a clarification
Gmail draft when an inbound reporter thread exists.
- `security-issue-import`: apply at *Reporter credited as*
(`From:`-header extraction) and at the ASF-relay `Credit:`-line
extraction; fold the clarification draft into the Step 7 receipt-
of-confirmation reply.
- `security-issue-import-from-pr`: apply at *Remediation developer*
(PR author resolution); no clarification draft (no inbound
reporter).
- `security-issue-import-from-md`: apply when the markdown carries a
finder-metadata line naming a specific handle.
- `security-issue-sync`: apply at the reporter-reply credit-mining
site (with clarification draft) and at the PR-author auto-append
for *Remediation developer*.
- `security-issue-deduplicate`: apply when consolidating credit
lines between two trackers; clarification draft on the drop
tracker's reporter thread if one exists.
- `generate-cve-json/SKILL.md`: add a "stays neutral; filter lives
upstream in the skills" note pointing at the policy doc, so the
upstream-vs-tool boundary is explicit and intentional human
overrides survive every JSON regeneration without a bypass flag.
Manual credits a human security-team member typed in are always
preserved verbatim -- the filter only fires on auto-extracted
candidates.
Generated-by: Claude Code (Opus 4.7)
---
.claude/skills/security-issue-deduplicate/SKILL.md | 16 ++
.../skills/security-issue-import-from-md/SKILL.md | 2 +-
.../skills/security-issue-import-from-pr/SKILL.md | 4 +-
.claude/skills/security-issue-import/SKILL.md | 6 +-
.claude/skills/security-issue-sync/SKILL.md | 14 +-
tools/vulnogram/bot-credits-policy.md | 221 +++++++++++++++++++++
tools/vulnogram/generate-cve-json/SKILL.md | 12 ++
7 files changed, 268 insertions(+), 7 deletions(-)
diff --git a/.claude/skills/security-issue-deduplicate/SKILL.md
b/.claude/skills/security-issue-deduplicate/SKILL.md
index 2610a8b..5163d0f 100644
--- a/.claude/skills/security-issue-deduplicate/SKILL.md
+++ b/.claude/skills/security-issue-deduplicate/SKILL.md
@@ -303,6 +303,22 @@ original report, earliest first)
confirmed, or the placeholder form when unconfirmed; the merge
does not silently re-synthesize credits)
+**Apply the [bot/AI credit
policy](../../../tools/vulnogram/bot-credits-policy.md)
+when consolidating.** If either tracker carries a credit line that
+matches the bot detection rule (`*[bot]` suffix, known-bot list,
+`*-bot`/`*-ai`/`*-agent`/`*-gpt` / `*scanner*` / `*automat*`
+suffix patterns, automation-name list), **do not** propagate that
+line into the kept tracker's *Reporter credited as* field. Instead,
+surface in the proposal *"skipped credit (during merge): `<line>`
+(matches bot policy — `<rule>`)"* with the source tracker number.
+If the drop tracker has an inbound reporter thread to reply on,
+also propose the policy's *clarification-reply* Gmail draft asking
+whether the bot/AI handle is the intended credit. The user can
+override per the policy doc. Manual credits that a human
+security-team member typed in (visible in the issue timeline) are
+always preserved verbatim — the filter only fires on credit lines
+that were auto-extracted upstream.
+
```markdown
### PR with the fix
diff --git a/.claude/skills/security-issue-import-from-md/SKILL.md
b/.claude/skills/security-issue-import-from-md/SKILL.md
index ca47e22..4ca0d7c 100644
--- a/.claude/skills/security-issue-import-from-md/SKILL.md
+++ b/.claude/skills/security-issue-import-from-md/SKILL.md
@@ -360,7 +360,7 @@ the role → concrete-name mapping comes from
| `**Repository:**` + `**Branch:**` | `Affected versions` | Literal text
*"`<owner>/<repo>` @ `<branch>` — versions to be confirmed during triage."* The
release-train mapping happens at allocation. |
| (auto) | `Security mailing list thread` | `N/A — imported from markdown file
<basename>; no security@ thread.` |
| (auto) | `Public advisory URL` | `_No response_`. |
-| (auto) | `Reporter credited as` | `_No response_`. The credit decision
happens at triage; if the file is AI-generated, there is typically no human
finder to credit. |
+| (auto) | `Reporter credited as` | `_No response_`. The credit decision
happens at triage; if the file is AI-generated, there is typically no human
finder to credit. If the markdown carries a `**Reporter:**` / `**Finder:**` /
`**Discovered by:**` metadata line naming a specific handle, **apply the
[bot/AI credit policy](../../../tools/vulnogram/bot-credits-policy.md)** before
lifting it into the field — when the policy fires (e.g. the markdown was
generated by an LLM scan and names the [...]
| `## Location` URL (when it points at a `<upstream>` PR) | `PR with the fix`
| The URL. Otherwise `_No response_` — the location commonly references a
vulnerable file, not a fix. |
| (auto) | `Remediation developer` | `_No response_`. |
| `**Category:**` | `CWE` | Literal value (free text); the actual CWE
assignment happens at triage / allocation. |
diff --git a/.claude/skills/security-issue-import-from-pr/SKILL.md
b/.claude/skills/security-issue-import-from-pr/SKILL.md
index 2bde220..b539833 100644
--- a/.claude/skills/security-issue-import-from-pr/SKILL.md
+++ b/.claude/skills/security-issue-import-from-pr/SKILL.md
@@ -359,7 +359,7 @@ has nine fields. Fill them as follows:
| **Public advisory URL** | `_No response_`. |
| **Reporter credited as** | `_No response_`. **The PR author is *not*
credited as the CVE reporter for this kind of import.** A public PR is not a
responsible disclosure — the contributor went straight to the public fix
without giving the security team a chance to coordinate the announcement, so
the security team neither owes a finder credit nor wants to incentivise the
practice. The user can populate the field manually if there is a
project-specific reason to credit a different individ [...]
| **PR with the fix** | `pr.url` (e.g.
`https://github.com/<upstream>/pull/65703`). |
-| **Remediation developer** | `pr.author.name` (fall back to
`pr.author.login`). One name per line. |
+| **Remediation developer** | `pr.author.name` (fall back to
`pr.author.login`). One name per line. **Apply the [bot/AI credit
policy](../../../tools/vulnogram/bot-credits-policy.md) before populating** —
if the PR author handle matches the bot detection rule (`*[bot]` suffix,
known-bot list, `*-bot`/`*-ai`/`*-agent`/`*-gpt` suffix patterns), leave the
field at `_No response_` and surface the skip in Step 6's proposal with the
matched rule (e.g. *"skipped credit: `dependabot[bot]` (match [...]
| **CWE** | `_No response_` (the team assesses; not derivable). |
| **Severity** | `Unknown`. |
| **CVE tool link** | `_No response_` (filled by
[`security-cve-allocate`](../security-cve-allocate/SKILL.md)). |
@@ -445,7 +445,7 @@ This tracker was deliberately opened by the security team
for a public fix that
**Next:** Step 6 — allocate the CVE via the
[`security-cve-allocate`](https://github.com/<tracker>/blob/<default-branch>/.claude/skills/security-cve-allocate/SKILL.md)
skill.
Provenance: public PR <pr.url>, author `@<pr.author.login>`.
-Extracted fields: scope=`<scope>`, *PR with the fix*=<pr.url>, *Remediation
developer*=<pr.author.name>, *Affected versions*=`<per-scope shape>`,
Severity=`Unknown`.
+Extracted fields: scope=`<scope>`, *PR with the fix*=<pr.url>, *Remediation
developer*=<pr.author.name> *(or `_No response_` + skip note when the PR author
matches the [bot/AI credit
policy](../../../tools/vulnogram/bot-credits-policy.md))*, *Affected
versions*=`<per-scope shape>`, Severity=`Unknown`.
*Reporter credited as* intentionally left blank — public-PR imports do not
credit the PR author as the CVE reporter (no responsible disclosure). See the
[Reporter credit
policy](https://github.com/<tracker>/blob/<tracker-default-branch>/.claude/skills/security-issue-import-from-pr/SKILL.md#reporter-credit-policy-for-public-pr-imports)
section of the skill for the rationale.
```
diff --git a/.claude/skills/security-issue-import/SKILL.md
b/.claude/skills/security-issue-import/SKILL.md
index 2abe7c0..941bbec 100644
--- a/.claude/skills/security-issue-import/SKILL.md
+++ b/.claude/skills/security-issue-import/SKILL.md
@@ -968,7 +968,7 @@ Decide the candidate's class from the root message:
| Class | How to spot it | How to handle |
|---|---|---|
| **Report**: a reporter describes a vulnerability | The body has a
description, a PoC / reproduction steps, an impact claim. Sender is an external
address (not `@apache.org`, not on the security-team roster in
[`AGENTS.md`](../../../AGENTS.md)). | Proceed to Step 4. |
-| **ASF-security relay**: `[email protected]` forwarded a report from a
reporter via the Foundation channel | Sender is `[email protected]`. The body
almost always starts with the ASF forwarding preamble — *"Dear PMC, The
security vulnerability report has been received by the Apache Security Team and
is being passed to you for action …"* — and contains the original report
underneath (often after a `====GHSA-…` separator when the report came in via
GitHub Security Advisory). The pream [...]
+| **ASF-security relay**: `[email protected]` forwarded a report from a
reporter via the Foundation channel | Sender is `[email protected]`. The body
almost always starts with the ASF forwarding preamble — *"Dear PMC, The
security vulnerability report has been received by the Apache Security Team and
is being passed to you for action …"* — and contains the original report
underneath (often after a `====GHSA-…` separator when the report came in via
GitHub Security Advisory). The pream [...]
| **Report (disposition converged)**: a `Report` where the inbound thread has
a team-member substantive technical disposition AND the reporter has
acknowledged it | Same body shape as `Report`, but the thread has a team-member
reply with one of: option-1/option-2 framing, *"we agree, opening fix PR"*
disposition, a docs-clarification acknowledgement; AND the reporter has replied
confirming the disposition; AND no further reporter follow-up is needed.
Detected at Step 3 by reading the thr [...]
| **CVE-tool bookkeeping**: an automated or human status-change notification
on the ASF CVE tool | Sender is `[email protected]` (or one of the
security-team members acting on behalf of the CVE tool). Subject matches one
of: `"CVE-YYYY-NNNNN reserved for airflow"`, `"Comment added on
CVE-YYYY-NNNNN"`, `"CVE-YYYY-NNNNN is now READY"`, `"CVE-YYYY-NNNNN is now
PUBLIC"`, `"CVE-YYYY-NNNNN is now PUBLISHED"`, `"CVE-YYYY-NNNNN REJECTED"`, or
a verbatim `"<state-change>"` line in the body poin [...]
| **Automated scanner dump**: SAST/DAST tool output, CodeQL/Dependabot alert
paste, a string of "issues" with no human PoC | Body is machine-generated,
contains multiple unrelated findings, no explanation of Security Model
violation | Surface as a candidate with class `automated-scanner` and **do
not** propose auto-import. In Step 5 the skill proposes a Gmail draft from the
*"Automated scanning results"* canned response in
[`canned-responses.md`](../../../<project-config>/canned-response [...]
@@ -1040,9 +1040,9 @@ here.
| **Affected versions** | Extract `Airflow <version>` / `>= X, < Y` / `<Y`
phrases from the body. 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-co [...]
| **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"
<[email protected]>`). This is a **placeholder** — the receipt-of-confirmation
reply in Step 7 asks the reporter to confirm their preferred credit form. |
+| **Reporter credited as** | The reporter's full display name from the email
`From:` header (e.g. `Alice Example` from `"Alice Example"
<[email protected]>`). This is a **placeholder** — the receipt-of-confirmation
reply in Step 7 asks the reporter to confirm their preferred credit form.
**Apply the [bot/AI credit
policy](../../../tools/vulnogram/bot-credits-policy.md) before populating** —
if the `From:`-header name or address matches the bot detection rule (`*[bot]`
suffix, known-bot l [...]
| **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. |
+| **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/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. |
| **Severity** | `Unknown`. Same reason as CWE — the team scores
independently. Surface a reporter-supplied CVSS / severity label in the
proposal's observed-state for context, but do not use it as the field value. |
| **CVE tool link** | `_No response_`. Filled at Step 6 once the CVE is
allocated. |
diff --git a/.claude/skills/security-issue-sync/SKILL.md
b/.claude/skills/security-issue-sync/SKILL.md
index 6066ef1..dff2a15 100644
--- a/.claude/skills/security-issue-sync/SKILL.md
+++ b/.claude/skills/security-issue-sync/SKILL.md
@@ -621,6 +621,18 @@ Process for finding the real reporter and the original
thread:
string ends up in the CVE record's `credits[]` and in the eventual
public advisory.
+ **Apply the [bot/AI credit
policy](../../../tools/vulnogram/bot-credits-policy.md)
+ to the extracted credit string** before proposing the update. If the
+ credit handle matches the bot detection rule (`*[bot]` suffix,
+ known-bot list, `*-bot`/`*-ai`/`*-agent`/`*-gpt` suffix patterns,
+ `noreply`/`security-alerts@` service sender), do **not** propose
+ landing the credit. Instead, surface in Step 2 *"skipped credit:
+ `<handle>` (matches bot policy — `<which rule fired>`)"* **and
+ propose a Gmail draft on the reporter's thread** per the policy's
+ *clarification-reply* step, asking whether the AI/bot handle is
+ the intended credit or whether there's a human behind it to credit
+ instead. The user can override the skip per the policy doc.
+
If the reporter has been *asked* the credit question but has not yet
responded, do not propose a change — leave the placeholder in place
and note in the proposal that the credit question is still pending a
@@ -703,7 +715,7 @@ update, label change, or next-step recommendation in Step 2:
| CVE record has open **review comments / reviewer proposals** (detected via
the Gmail-search path in Step 1e — reviewer-comment notifications from
Vulnogram land on `<security-list>` with the CVE ID in the subject line; the
`cveprocess.apache.org/cve5/<CVE-ID>.json` endpoint is behind ASF OAuth and is
not readable from this skill's context, so Gmail is the load-bearing signal
source). | Surface each open review comment in Step 2a with **clickable links**
to the Gmail thread and to the C [...]
| The referenced `<upstream>` PR has been opened but is still in `open` state
| Propose `pr created` label; update the *"PR with the fix"* body field with
the PR URL. |
| The referenced `<upstream>` PR moved to `merged` | Propose swapping `pr
created` → `pr merged`; update milestone to the shipping release if now known.
**Also**: check whether all six mandatory CVE body fields are populated (*CWE*,
*Affected versions*, *Severity*, *Reporter credited as*, *Short public summary
for publish*, *PR with the fix*). If any is empty / `_No response_`, propose
posting (or PATCH-updating) the *Remediation-developer fill-fields comment* per
[the dedicated bullet i [...]
-| The *"PR with the fix"* body field has at least one PR URL **and** the
*"Remediation developer"* body field is missing the PR author's name (or is
`_No response_`) | Propose appending the PR author's display name (`gh pr view
<N> --repo <upstream> --json author --jq '.author.name // .author.login'`) to
the *"Remediation developer"* body field. **Append, never overwrite** — manual
edits (co-authors added by the triager, name spelling corrections, "Anonymous"
overrides) must survive subs [...]
+| The *"PR with the fix"* body field has at least one PR URL **and** the
*"Remediation developer"* body field is missing the PR author's name (or is
`_No response_`) | Propose appending the PR author's display name (`gh pr view
<N> --repo <upstream> --json author --jq '.author.name // .author.login'`) to
the *"Remediation developer"* body field. **Append, never overwrite** — manual
edits (co-authors added by the triager, name spelling corrections, "Anonymous"
overrides) must survive subs [...]
| The *"Affected versions"* body field is missing, holds a pre-convention
shape, or carries the project's pre-release sentinel, and the tracker is
**not** at `fix released` yet | Propose populating / refining *"Affected
versions"* per the project's convention. The per-scope shape, the pre-release
sentinel (if any), and the lifecycle live in
[`<project-config>/scope-labels.md` — *Affected versions convention by
scope*](../../../<project-config>/scope-labels.md#affected-versions-convention
[...]
| A tracker is transitioning to `fix released` (per the row below) and
*"Affected versions"* still carries the project's pre-release sentinel |
Propose replacing the sentinel with the concrete released version per the
project's convention; see [`<project-config>/scope-labels.md` — *Affected
versions convention by
scope*](../../../<project-config>/scope-labels.md#affected-versions-convention-by-scope)
for the recipe. After the body update, regenerate the CVE JSON attachment so
`versions[] [...]
| 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#det [...]
diff --git a/tools/vulnogram/bot-credits-policy.md
b/tools/vulnogram/bot-credits-policy.md
new file mode 100644
index 0000000..f13c421
--- /dev/null
+++ b/tools/vulnogram/bot-credits-policy.md
@@ -0,0 +1,221 @@
+<!-- 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)*
+
+- [CVE-credit policy for bot / AI
accounts](#cve-credit-policy-for-bot--ai-accounts)
+ - [Why](#why)
+ - [Detection — when does the rule fire?](#detection--when-does-the-rule-fire)
+ - [Default behaviour — what the skills do when the rule
fires](#default-behaviour--what-the-skills-do-when-the-rule-fires)
+ - [Where the rule applies](#where-the-rule-applies)
+ - [Where the rule does NOT apply](#where-the-rule-does-not-apply)
+ - [Worked examples](#worked-examples)
+
+<!-- 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 -->
+
+# CVE-credit policy for bot / AI accounts
+
+The CVE record's `credits[]` array records *people* who discovered a
+vulnerability (`type: "finder"`) or shipped the fix
+(`type: "remediation developer"`). When an obvious bot or AI account
+appears as the candidate for either role, the default is to **not**
+credit them. The skills that populate the *Reporter credited as* and
+*Remediation developer* tracker body fields enforce this rule at the
+point of extraction, and ask the user before adding the credit when
+the rule matches.
+
+This file is the single source of truth for *who counts as a bot* and
+*how the skills behave when one is detected*. Skills reference it
+instead of duplicating the heuristic.
+
+## Why
+
+* Bot accounts (Dependabot, Renovate, GHSA Probot, GitHub Actions,
+ Snyk, Mend, …) act on behalf of an organisation or a piece of
+ automation — they are not the *person* who found the bug or fixed
+ it. Naming them as `finder` or `remediation developer` in a public
+ CVE record misrepresents authorship and pollutes the CNA-feed
+ with handles that are not actionable as contributor credit.
+* AI / LLM agents that opened a PR or filed a report are tools the
+ human used. The human who drove the agent — if there is one
+ identifiable in the thread — is the candidate for credit. The
+ agent itself never is.
+* Forwarding services (`[email protected]`,
+ `security-alerts@<scanner>.com`, …) sit between the actual
+ reporter and us; their address is a relay, not an identity.
+
+## Detection — when does the rule fire?
+
+A handle or email is *obvious bot/AI* when **any** of these match:
+
+1. **GitHub `[bot]` suffix** — handle ends in `[bot]`. The canonical
+ GitHub convention (e.g. `dependabot[bot]`, `github-actions[bot]`,
+ `renovate[bot]`, `copilot[bot]`, `ghsa-probot[bot]`).
+2. **Known-bot / automation-name list** — case-insensitive match on
+ the handle's un-bracketed stem **or** on a whole-word occurrence
+ in a free-form credit string (e.g. *"discovered by Automated
+ Scanner v3"*) against:
+ `dependabot`, `renovate`, `snyk-bot`, `snyk`, `copilot`,
+ `ghsa-probot`, `github-actions`, `mend-bot`, `mend`, `whitesource`,
+ `sonatype-lift`, `lift-bot`, `codecov`, `mergify`, `mergifyio`,
+ `allcontributors`, `fossabot`, `imgbotapp`, `pre-commit-ci`,
+ `claude`, `chatgpt`, `gpt-bot`, `gpt`, `anthropic`, `openai`,
+ `automated`, `automation`, `scanner`, `auto-scanner`,
+ `vulnerability-scanner`, `security-scanner`, `sast`, `dast`.
+3. **Suffix / contains-pattern handles** — handle matches one of
+ these case-insensitive regex patterns:
+ * `*-bot`, `*bot` (e.g. `securitybot`, `release-bot`)
+ * `*-ai`, `*ai` (e.g. `scan-ai`, `securityai`)
+ * `*-agent`, `*agent` (e.g. `triage-agent`)
+ * `*-gpt`, `*gpt` (e.g. `secaudit-gpt`)
+ * `*scanner*`, `*-scan` (e.g. `securityscanner-7`, `audit-scan`)
+ * `*automat*` (e.g. `automated-triage`, `automation-svc`,
+ `automaton`)
+4. **Service email senders** — the `From:` address local-part
+ contains `noreply`, `no-reply`, `donotreply`, or starts with
+ `bot@`, `security-alerts@`, `notifications@`,
+ `mailer-daemon@` (these are relays, never identities).
+
+The detection is intentionally broad. False positives are cheap (the
+user is shown what was skipped and can override with one word —
+*"include X"*); false negatives are expensive (a bot lands in a
+public CVE record).
+
+## Default behaviour — what the skills do when the rule fires
+
+Each skill that extracts a credit candidate applies these rules at
+extraction time:
+
+1. **Skip silently in the data flow.** Do not write the bot's name
+ into the *Reporter credited as* or *Remediation developer* body
+ field. Leave the field at its current value (typically
+ `_No response_` for a fresh tracker, or whatever the prior
+ resolved value was for an existing tracker).
+2. **Surface the skip in the user-facing proposal.** Include a
+ one-line entry in the proposal's "skipped" section of the shape:
+
+ ```text
+ skipped credit: <handle> (matches bot policy — ends with [bot])
+ ^^^^^ which rule matched
+ skipped credit: <handle> (matches bot policy — in known-bot list)
+ skipped credit: <handle> (matches bot policy — *-bot suffix pattern)
+ skipped credit: <email> (matches bot policy — noreply service sender)
+ ```
+
+ The reason — *which* rule fired — is mandatory; it lets the user
+ judge whether the skip is correct.
+3. **Honour an explicit override.** The user may override the skip
+ for a specific tracker with any of these phrasings (or obvious
+ variants):
+
+ * *"include `<handle>` anyway"*
+ * *"credit `<handle>` as finder"*
+ * *"credit `<handle>` as remediation developer"*
+ * *"yes, add `<handle>`"*
+
+ When the user confirms the override, set the appropriate body
+ field exactly as the user dictated. Do not auto-extend the
+ override to other trackers — overrides are per-tracker.
+
+4. **Draft a clarification reply to the reporter when an email
+ thread exists.** When the candidate would have been credited as
+ the *finder* (i.e. the reporter themselves is the bot-looking
+ name) **and** the tracker has an inbound `<security-list>` mail
+ thread to reply on, propose a **Gmail draft** (not a sent
+ message) on the same thread asking whether the bot/AI handle is
+ the intended credit or whether there's a human behind it who
+ should be credited instead. The draft should be:
+
+ * **Polite and short** — one or two short paragraphs; no
+ accusations, no jargon.
+ * **Specific** — name the handle that was detected and which
+ rule fired (so the reporter sees the same reasoning the
+ security team did).
+ * **Actionable** — offer two clear paths: *"credit `<handle>`
+ as-is"* or *"credit `<human-name>` instead — please reply
+ with the preferred attribution"*.
+ * **Sent only after explicit user confirmation** per the
+ [framework's never-send-without-asking rule](../../AGENTS.md).
+
+ When the tracker has no inbound mail thread (e.g. a
+ `security-issue-import-from-pr` tracker), skip the draft step —
+ there is no reporter to ask.
+
+ A reusable template for the draft body lives in
+
[`<project-config>/canned-responses.md`](../../<project-config>/canned-responses.md);
+ if no project-local template exists yet, generate the body
+ inline and propose adding it to the canned-responses file as a
+ follow-up.
+
+## Where the rule applies
+
+This policy fires at every site where the suite *auto-extracts* a
+credit candidate without explicit user instruction:
+
+| Skill | Extraction site |
+|---|---|
+|
[`security-issue-import`](../../.claude/skills/security-issue-import/SKILL.md)
| Reporter name from email `From:` header; ASF-relay `Credit:` line |
+|
[`security-issue-import-from-pr`](../../.claude/skills/security-issue-import-from-pr/SKILL.md)
| PR author → *Remediation developer* |
+|
[`security-issue-import-from-md`](../../.claude/skills/security-issue-import-from-md/SKILL.md)
| Reporter / finder name from markdown metadata |
+| [`security-issue-sync`](../../.claude/skills/security-issue-sync/SKILL.md) |
Reporter credit mined from email replies; PR author auto-append to *Remediation
developer* |
+|
[`security-issue-deduplicate`](../../.claude/skills/security-issue-deduplicate/SKILL.md)
| Credit consolidation from two trackers |
+
+The rule does **not** fire when the user *explicitly* types a name
+into a credit field, or when a tracker already carries a credit that
+a human security-team member set — those are explicit decisions and
+the skill respects them.
+
+## Where the rule does NOT apply
+
+* [`generate-cve-json`](generate-cve-json/SKILL.md) stays neutral.
+ Whatever is in the tracker's credit fields is what lands in the
+ CVE JSON. The filter is upstream, at the skills, so that an
+ intentional human override survives JSON regeneration.
+* The *PR with the fix* body field on a tracker still records the
+ PR even when its author is a bot — the field captures the
+ artifact, not the author. Only *Remediation developer* is
+ bot-filtered.
+
+## Worked examples
+
+**`security-issue-import-from-pr` import of a Dependabot PR.** The
+PR's author is `dependabot[bot]`. The skill skips the
+*Remediation developer* assignment and surfaces:
+`skipped credit: dependabot[bot] (matches bot policy — ends with
+[bot])`. The user can override with *"credit dependabot[bot] as
+remediation developer"* if a real human at Dependabot HQ is owed the
+credit (unusual but allowed).
+
+**`security-issue-sync` mining a reporter email reply.** The
+reporter wrote *"please credit me as claude-bot, I used Claude
+Code"*. The skill skips the credit, surfaces `skipped credit:
+claude-bot (matches bot policy — *-bot suffix pattern)`, **and
+proposes a Gmail draft** on the original report thread asking
+*"the credit you suggested (`claude-bot`) reads like an AI/agent
+handle — would you prefer we credit you under your name (e.g. the
+one on the original report) instead, or is `claude-bot` the
+attribution you want?"* The user reviews the draft and approves
+the send. If the reporter replies with a human name, sync picks
+it up on the next pass.
+
+**`security-issue-import` from a relay address.** The `From:`
+header is `[email protected]`. The skill skips
+*Reporter credited as*, surfaces `skipped credit:
[email protected] (matches bot policy — noreply
+service sender)`, and prompts the user for the actual reporter
+identity (typically findable inside the email body).
+
+**`security-issue-import` of an ASF-relay report with an
+automation credit line.** The forwarded body ends with *"This
+vulnerability was discovered and reported by Automated Security
+Scanner v3 (run by ACME Sec Team)"*. The skill matches both the
+`automated` known-name and the `*scanner*` contains-pattern,
+skips *Reporter credited as*, surfaces `skipped credit:
+"Automated Security Scanner v3" (matches bot policy — known
+automation name + *scanner* pattern)`, and routes the
+credit-preference question to `@raboof` / Arnout via the
+ASF-relay credit-preference flow. The user can override with
+*"credit ACME Sec Team as finder"* if the human team behind the
+scanner is owed the credit.
diff --git a/tools/vulnogram/generate-cve-json/SKILL.md
b/tools/vulnogram/generate-cve-json/SKILL.md
index 5c312f1..7e83ede 100644
--- a/tools/vulnogram/generate-cve-json/SKILL.md
+++ b/tools/vulnogram/generate-cve-json/SKILL.md
@@ -190,6 +190,18 @@ filled in through a prior `security-issue-sync` run. In
particular:
there). The `--remediation-developer` CLI flag adds further names
on top of whatever the body already lists.
+> **Bot / AI credit policy.** This generator is intentionally
+> neutral on credit content: whatever a tracker's *Reporter credited
+> as* or *Remediation developer* field carries is what lands in
+> `credits[]`. The filtering of obvious bot / AI accounts (e.g.
+> `dependabot[bot]`, `*-scanner`, `automated-*`) happens **upstream
+> in the skills** at extraction time — see
+> [`bot-credits-policy.md`](../bot-credits-policy.md) for the
+> detection rule and the per-skill enforcement sites. Keeping the
+> filter upstream means an intentional human override (typed
+> directly into the field) survives every JSON regeneration without
+> needing a special bypass flag here.
+
- **CWE** — `CWE-285: Improper Authorization` style works; so does a bare
`CWE-285` or a plain sentence. The script extracts the `CWE-\d+` token
for the `cweId` field and uses the rest as the human-readable