diff --git a/.claude/skills/allocate-cve/SKILL.md b/.claude/skills/allocate-cve/SKILL.md index 2fe5b392..b4b4c490 100644 --- a/.claude/skills/allocate-cve/SKILL.md +++ b/.claude/skills/allocate-cve/SKILL.md @@ -50,9 +50,9 @@ confirm. The only action the skill performs unilaterally is **reading** the tracker state and printing the allocation recipe for the user to click through. -**Golden rule — only Apache Airflow PMC members can allocate CVEs.** +**Golden rule — only members of the project's PMC can allocate CVEs.** The ASF Vulnogram form at `https://cveprocess.apache.org/allocatecve` -requires ASF OAuth with PMC-level access on the Airflow project. The +requires ASF OAuth with PMC-level access on the adopting project. The full allocation mechanics (form-fill recipe, PMC-gated access, form fields, fatal mis-allocation, after-allocation wire-back) live in [`tools/vulnogram/allocation.md`](../../../tools/vulnogram/allocation.md); @@ -61,8 +61,8 @@ the per-project URL templates live in This is not something the skill can work around — a non-PMC user who clicks *Allocate* sees the button grey out. -The current Airflow PMC roster lives on the ASF project page: -. Authoritative +The current PMC roster lives on the project's ASF committee page +(`https://projects.apache.org/committee.html?`). Authoritative GitHub handles for the subset of PMC members who also sit on the security team are listed in [`/release-trains.md`](../../..//release-trains.md) @@ -113,7 +113,7 @@ anything else. required if the tracker carries a reporter thread that needs a status-update draft (Step 5). - **A PMC member on call** — the Vulnogram allocation form is - PMC-gated. If the user is not on the Airflow PMC, the skill + PMC-gated. If the user is not on the project's PMC, the skill still runs: it produces a relay message for a PMC member to click through instead of stopping. @@ -130,7 +130,7 @@ Before touching the tracker, verify: 1. **`gh` is authenticated** — `gh api repos/ --jq .name` must return - `airflow-s`. A 401/403/404 means the user needs `gh auth login` + ``. A 401/403/404 means the user needs `gh auth login` or collaborator access; stop. 2. **`uv` is on the PATH** — `uv --version`. Without it the Step 4 CVE-JSON regeneration would fail silently mid-flow; better to @@ -580,7 +580,7 @@ presenting. nothing more. Do not try to automate the form fill — the ASF CVE tool is ASF-OAuth-gated and agent automation of CNA allocation is explicitly out of scope. -- **Only an Airflow PMC member can allocate.** The Vulnogram +- **Only a project PMC member can allocate.** The Vulnogram allocation button is PMC-gated. If the user running this skill is not a PMC member, the recipe is a **relay message** they post for a PMC member to act on, not a form they can fill themselves. diff --git a/.claude/skills/deduplicate-security-issue/SKILL.md b/.claude/skills/deduplicate-security-issue/SKILL.md index 8fea8865..809df450 100644 --- a/.claude/skills/deduplicate-security-issue/SKILL.md +++ b/.claude/skills/deduplicate-security-issue/SKILL.md @@ -102,7 +102,7 @@ in `README.md`. ## Step 0 — Pre-flight check 1. `gh api repos/ --jq .name` returns - `airflow-s`. + ``. 2. Both issue numbers resolve — `gh issue view --repo --json number` and the same for `` — before any write. diff --git a/.claude/skills/fix-security-issue/SKILL.md b/.claude/skills/fix-security-issue/SKILL.md index 883c6fda..63f7291b 100644 --- a/.claude/skills/fix-security-issue/SKILL.md +++ b/.claude/skills/fix-security-issue/SKILL.md @@ -9,7 +9,7 @@ description: | implementation plan, waits for explicit user confirmation, writes the change in the user's local clone, runs the local checks and tests, opens a PR from the user's fork via `gh pr create --web`, - and updates the airflow-s tracking issue with the new PR link and any + and updates the tracking issue with the new PR link and any relevant labels. Public PR content is checked to make sure it does **not** reveal the CVE, the security nature of the change, or any link back to . @@ -138,7 +138,7 @@ continue. 1. **`gh` authenticated** — `gh api repos/ --jq .name` and `gh api repos/ --jq .name` both return. A 401/403 - on the first means no airflow-s access; on the second it is a + on the first means no access; on the second it is a quota/auth issue — both require user action, stop. 2. **Fork exists and is pushable** — `gh repo view /airflow --json name --jq .name` @@ -256,7 +256,7 @@ If **easily fixable**, extract and write down: - the target branch (`main` almost always; a release branch only if the user explicitly says so), - any backport label that should be applied to the eventual PR, based - on the milestone on the `airflow-s` issue (the adopting project's + on the milestone on the `` issue (the adopting project's backport-label policy and current release branches live in [`/fix-workflow.md`](../../..//fix-workflow.md#backport-labels) and @@ -330,7 +330,7 @@ verbatim.** - **bad** (reveals security framing): `cve-2026-40690`, `security-fix-218`, `vulnerable-deserialize-fix`. - Tracker identifiers on their own (e.g. `airflow-s-216`) are not + Tracker identifiers on their own (e.g. `-216`) are not flagged — they are public-safe identifiers per the [Confidentiality of the tracker repository](../../../AGENTS.md#confidentiality-of-the-tracker-repository) rule — but they also do not help anyone reading the branch URL @@ -394,7 +394,7 @@ List: ### 4e. Backport label -If the `airflow-s` issue's milestone indicates a release branch that +If the `` issue's milestone indicates a release branch that has not yet been cut (e.g. `3.1.9`, `3.2.1`), note which `backport-to-vX-Y-test` label the PR should carry so that the fix lands on the intended patch release. If no backport is needed (the @@ -520,7 +520,7 @@ only fires on merge, not on label application, so there is no race with CI. Applying the label early ensures it is not forgotten. **Grep the PR body one more time for forbidden terms** (`CVE`, -`airflow-s`, `vulnerability`, `security fix`, `advisory`, private +``, `vulnerability`, `security fix`, `advisory`, private issue number, reporter name tied to a finding) before calling `gh pr create --web`. If anything matches, abort and tell the user. @@ -530,7 +530,7 @@ After the user submits the PR in the browser, capture the PR URL --- -## Step 9 — Update the airflow-s tracking issue +## Step 9 — Update the tracking issue Now that a public PR exists, update the private tracking issue: @@ -565,7 +565,7 @@ Now that a public PR exists, update the private tracking issue: The public `` PR description and any follow-up public comments must also obey the rule, but under the usual public-surface confidentiality constraints (no `CVE-`, - `airflow-s`, *"security fix"*, etc. alongside the mention). + ``, *"security fix"*, etc. alongside the mention). 2. **Update the issue body "PR with the fix" field** if it is empty or points to a stale PR. Use `gh issue view --json body`, patch @@ -575,7 +575,7 @@ Now that a public PR exists, update the private tracking issue: 3. **Maintain milestones and labels** — see the next section. -4. **Status update to the reporter** — if the airflow-s issue has an +4. **Status update to the reporter** — if the issue has an identified external reporter and the reporter has not yet been told about the fix PR, delegate to the `sync-security-issue` skill's "Status update to the reporter" category by re-running @@ -720,7 +720,7 @@ Print a short recap: - the branch name (in the user's fork), - the list of files changed, - the tests that were run and their results, -- the comment posted on the `airflow-s` issue, +- the comment posted on the `` issue, - the backport label that was applied (or a note that none was needed), - the next step — typically *"wait for review; re-run sync-security-issue after the PR merges to transition the issue diff --git a/.claude/skills/import-security-issue-from-pr/SKILL.md b/.claude/skills/import-security-issue-from-pr/SKILL.md index 560e3f03..154f5ce2 100644 --- a/.claude/skills/import-security-issue-from-pr/SKILL.md +++ b/.claude/skills/import-security-issue-from-pr/SKILL.md @@ -379,7 +379,7 @@ The first entry on the tracker's status rollup. Shape per [`tools/github/status-rollup.md`](../../../tools/github/status-rollup.md): ```markdown - +
· @ · Import from PR (, #) **Imported from public PR `#` on ** (scope: ``, PR state: ``). @@ -391,7 +391,7 @@ This tracker was deliberately opened by the security team for a public fix that Provenance: public PR , author `@`. Extracted fields: scope=``, *PR with the fix*=, *Remediation developer*=, *Affected versions*=``, 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//blob/airflow-s/.claude/skills/import-security-issue-from-pr/SKILL.md#reporter-credit-policy-for-public-pr-imports) section of the skill for the rationale. +*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//blob//.claude/skills/import-security-issue-from-pr/SKILL.md#reporter-credit-policy-for-public-pr-imports) section of the skill for the rationale. ``` Zero-whitespace rules from diff --git a/.claude/skills/import-security-issue/SKILL.md b/.claude/skills/import-security-issue/SKILL.md index 4708ba70..94b8d7ef 100644 --- a/.claude/skills/import-security-issue/SKILL.md +++ b/.claude/skills/import-security-issue/SKILL.md @@ -162,7 +162,7 @@ candidate Gmail threads: | Selector | Resolves to | |---|---| -| `import new` (default) | every security@ thread received in the last **14 days** that has not yet been imported as an airflow-s issue and has not already been answered-and-closed on-thread | +| `import new` (default) | every security@ thread received in the last **14 days** that has not yet been imported as an issue and has not already been answered-and-closed on-thread | | `import since:YYYY-MM-DD` | every security@ thread received since the given date that is not yet imported | | `import thread:` | the single Gmail thread with that `threadId` — useful for re-importing after a manual discard, or for picking up a single message the automatic scan missed | | `import last 30d` / `import all` / `import last 90d` (explicit request only) | a wider sweep — use when the skill has not been run in a while or the user is doing a backlog catch-up. The `all` alias is 90 days. | @@ -257,7 +257,7 @@ most threads will be filtered out at Step 2. --- -## Step 2 — Deduplicate against existing airflow-s issues +## Step 2 — Deduplicate against existing issues For each candidate `threadId`, check whether that ID already appears in an `` issue body. The sync skill records each thread @@ -323,7 +323,7 @@ check: cannot review it"* / *"We do not consider this a vulnerability"* / *"We do not consider this a security issue"* - - *"Per the Airflow Security Model"* / *"documented in our + - *"Per the project's security model"* / *"documented in our Security Model"* / *"this is by design"* / *"this is expected behaviour"* - *"This is explicitly out of scope"* / *"is explicitly @@ -544,8 +544,8 @@ mailbox. The per-candidate budget is ≤ 2 archive searches Hits whose author is on the security-team roster AND whose body opens with a canned-response cue (*"Thank you for reporting … - this isn't a security issue"*, *"Per the Airflow Security - Model"*, *"This is expected behaviour for a Dag author"*, etc.) + this isn't a security issue"*, *"Per the project's security + model"*, *"This is documented / expected behaviour"*, etc.) are prior rejections. Fetch each with `mcp__claude_ai_Gmail__get_thread` (MINIMAL is enough when you only need to confirm the canned-response shape; FULL_CONTENT is @@ -1015,7 +1015,7 @@ For each confirmed `Report` / `ASF-security relay`: `gh issue comment --repo --body-file `: ```markdown - +
· @ · Import (, ) **Imported from Gmail thread `` on ** (class: ``, reporter: ``). diff --git a/.claude/skills/invalidate-security-issue/SKILL.md b/.claude/skills/invalidate-security-issue/SKILL.md index 2fba5f61..598b81ee 100644 --- a/.claude/skills/invalidate-security-issue/SKILL.md +++ b/.claude/skills/invalidate-security-issue/SKILL.md @@ -174,7 +174,7 @@ the close. Read the *Security mailing list thread* body field: For `security@`-imported trackers, locate the Gmail `threadId`: 1. Read the rollup comment on the tracker (the first - `
` block with the `airflow-s status rollup v1` + `
` block with the ` status rollup v1` marker). Look for `threadId` references in the *Provenance:* line of the import entry. 2. If the rollup is missing or thin, fall back to a Gmail subject @@ -197,7 +197,7 @@ Scan `tracker.comments[]` for posts that argue **why** the report is not a security issue. Strong signals: - Citations of the - [Apache Airflow Security Model]() + [the project's security model]() (full URL, anchor links, paraphrases). - Phrases like *"this is by design"*, *"out of scope"*, *"documented behavior"*, *"requires X privileges already"*, @@ -292,7 +292,7 @@ Reasoning summary in the [status rollup](#issuecomment-); a draft rep For PR-imported trackers, replace *"a draft reply to the reporter is in Gmail awaiting review"* with *"no reporter notification (PR-imported tracker — see the import-from-pr skill's -[Reporter credit policy](https://github.com//blob/airflow-s/.claude/skills/import-security-issue-from-pr/SKILL.md#reporter-credit-policy-for-public-pr-imports))"*. +[Reporter credit policy](https://github.com//blob//.claude/skills/import-security-issue-from-pr/SKILL.md#reporter-credit-policy-for-public-pr-imports))"*. The comment links must resolve once the rollup entry from Step 5e has been posted (capture its URL and substitute before posting @@ -416,11 +416,11 @@ upsert recipe). Shape: - @: > ([source](#issuecomment-)) - ... -**Canned response selected:** ** in [`canned-responses.md`](https://github.com//blob/airflow-s//canned-responses.md#). +**Canned response selected:** ** in [`canned-responses.md`](https://github.com//blob///canned-responses.md#). **Reporter notification:** - **`security@`-imported:** Gmail draft `` created on thread `` — awaiting user review. -- **PR-imported:** none (no reporter; per [Reporter credit policy](https://github.com//blob/airflow-s/.claude/skills/import-security-issue-from-pr/SKILL.md#reporter-credit-policy-for-public-pr-imports)). +- **PR-imported:** none (no reporter; per [Reporter credit policy](https://github.com//blob//.claude/skills/import-security-issue-from-pr/SKILL.md#reporter-credit-policy-for-public-pr-imports)). - **Indeterminate import path:** none (flag from Step 2 surfaced; user explicitly chose silent close). **Project board:** archived (item ``). diff --git a/.claude/skills/sync-security-issue/SKILL.md b/.claude/skills/sync-security-issue/SKILL.md index bad2e1d7..f3e74256 100644 --- a/.claude/skills/sync-security-issue/SKILL.md +++ b/.claude/skills/sync-security-issue/SKILL.md @@ -302,7 +302,7 @@ Before reading any tracker state, verify: always a stop. 2. **`gh` is authenticated** with access to `` — `gh api repos/ --jq .name` must return - `airflow-s`. A 401/403/404 means the user needs + ``. A 401/403/404 means the user needs `gh auth login` or collaborator access. 3. **PonyMail MCP status** (opt-in; primary read path when enabled) — read `config/user.md` → `tools.ponymail`. If @@ -405,7 +405,7 @@ state. `gh search prs --state` only accepts `open` or `closed`, so run two queries (or omit `--state` entirely for "any state"): ```bash -gh search prs "airflow-s#" --repo --json number,title,state,url,milestone,mergedAt +gh search prs "#" --repo --json number,title,state,url,milestone,mergedAt gh search prs "#" --repo --json number,title,state,url,milestone,mergedAt ``` @@ -474,7 +474,7 @@ Process for finding the real reporter and the original thread: 1. **Do not stop at the GitHub-notification mirror thread.** Searching Gmail for the issue title typically returns the GitHub-notification thread (`From: via security <>`, - `To: `) first. That is + `To: <>`) first. That is *not* the original report — it is a mirror of the GitHub issue and its comments. Filter it out and keep digging. @@ -619,8 +619,8 @@ update, label change, or next-step recommendation in Step 2: | Reporter explicit opt-out of credit (*"do not credit me"*, *"anonymous"*) | Set the field to `anonymous` and flag the advisory to use that form. | | Release manager's `[RESULT][VOTE] Release Airflow ` on `` for a version that carries the fix | Record the release manager in the "Known release managers" subsection of [`AGENTS.md`](../../../AGENTS.md) if not already there; flag Step 13 (advisory) as assigned to that person. | | Advisory message sent to `announce@apache.org` / `` for the CVE on the tracker | Propose adding the `announced - emails sent` label and removing `fix released`. **Do not propose closing the issue here** — closing is gated on the archived public advisory URL being captured (see the next row). | -| Advisory archived on `` (the announcement message is now visible in `lists.apache.org/list.html?` — scan the archive with the CVE ID when `announced - emails sent` is set and the *"Public advisory URL"* body field is empty) | Propose populating the *"Public advisory URL"* body field with the archive URL, regenerating the CVE JSON attachment (the generator picks the URL up automatically and tags it `vendor-advisory`), adding the `announced` label, **and moving the project-board column from `Fix released` to `Announced`** on [`` Project 2](https://github.com/orgs/airflow-s/projects/2). The `Announced` column is the board's representation of Step 14 — the advisory has landed and the CVE record is staged with `CNA_private.state = "PUBLIC"` ready for the release manager's single-paste Step 15. **Do not close the issue and do not add the `vendor-advisory` label** — that is Step 15, owned by the release manager after they move the record to PUBLIC in Vulnogram. | -| Project-board column drifted from the issue's label-derived state (e.g. a tracker carries `pr merged` but is still in the `PR created` column on [Project 2](https://github.com/orgs/airflow-s/projects/2), or `announced` + *Public advisory URL* body field populated but the column is still `Fix released`) | Propose moving the project item to the correct column per the mapping table in Step 2b. The board is the primary security-team overview surface; a stale column hides ownership handoffs from the team at a glance. | +| Advisory archived on `` (the announcement message is now visible in `lists.apache.org/list.html?` — scan the archive with the CVE ID when `announced - emails sent` is set and the *"Public advisory URL"* body field is empty) | Propose populating the *"Public advisory URL"* body field with the archive URL, regenerating the CVE JSON attachment (the generator picks the URL up automatically and tags it `vendor-advisory`), adding the `announced` label, **and moving the project-board column from `Fix released` to `Announced`** on [`` Project 2](). The `Announced` column is the board's representation of Step 14 — the advisory has landed and the CVE record is staged with `CNA_private.state = "PUBLIC"` ready for the release manager's single-paste Step 15. **Do not close the issue and do not add the `vendor-advisory` label** — that is Step 15, owned by the release manager after they move the record to PUBLIC in Vulnogram. | +| Project-board column drifted from the issue's label-derived state (e.g. a tracker carries `pr merged` but is still in the `PR created` column on [Project 2](), or `announced` + *Public advisory URL* body field populated but the column is still `Fix released`) | Propose moving the project item to the correct column per the mapping table in Step 2b. The board is the primary security-team overview surface; a stale column hides ownership handoffs from the team at a glance. | | `announced` label set and CVE record on `cveprocess.apache.org` now reports state PUBLISHED (checked via `curl -s https://cveprocess.apache.org/cve5/.json` / the ASF CVE tool API, or an explicit release-manager comment on the issue stating the Vulnogram push is done) | Propose closing the issue. Do not update any labels. This is the terminal transition. | | CVE record has open **review comments / reviewer proposals** (detected via the Gmail-search path in Step 1e — reviewer-comment notifications from Vulnogram land on `` with the CVE ID in the subject line; the `cveprocess.apache.org/cve5/.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 CVE record on `cveprocess.apache.org` (the reader can authenticate in-browser to see live state), verbatim-quoted; then for each one that maps cleanly to a tracking-issue body field (CWE, Affected versions, Reporter credited as, Public advisory URL, Short public summary), **propose the matching body-field update** as a numbered item in Step 2b. The body is the source of truth for the CVE JSON — regeneration in Step 5 will pull the update back into the paste-ready attachment, and the release manager's only remaining action is the Vulnogram paste + comment-resolution click. Comments that do not map to a body field (severity/CVSS, out-of-scope challenges, free-form rewrites) are surfaced verbatim and flagged for human decision. See Step 1e for the full Gmail-search recipe and the reviewer-comment-to-field mapping table. | | The referenced `` 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. | @@ -982,7 +982,7 @@ will change and *why*. Group them by category: - **Assignees** — when a fix PR exists in `` (found in Step 1b or named in the *"PR with the fix"* body field) **and the - PR author is a member of the Airflow security team** (their GitHub + PR author is a member of the project security team** (their GitHub handle appears in the security-team roster in [`/release-trains.md`](../../..//release-trains.md) — when in doubt, run `gh api repos//collaborators --jq '.[].login'` @@ -1460,7 +1460,7 @@ the actual person, in this order: [`AGENTS.md`](../../../AGENTS.md) first** — if the release is already listed there, use that name. This is the cache; the next two sources are how the cache was populated and how you refresh it. -2. **Check the Airflow Release Plan wiki** at +2. **Check the project's release plan** at . This is the canonical forward-looking schedule for every release train (core Airflow, Providers, Airflow Ctl, Helm Chart, Airflow 2) @@ -1523,7 +1523,7 @@ line so the handoff is unambiguous: > JSON embed). **Whenever a CVE ID is mentioned** — in the proposal, in the status-change -comment on the `airflow-s` issue, in the draft email to the reporter, or in +comment on the `` issue, in the draft email to the reporter, or in the recap — render it as a clickable link per the "Linking CVEs" section of [`AGENTS.md`](../../../AGENTS.md). Concretely: @@ -1811,7 +1811,7 @@ After the regeneration step finishes, print a short recap: the entire recap text: any mention of the tracking issue, any cross-referenced `` issue, any PR, any specific comment anchor and any milestone must be a clickable markdown link. -The user has to be able to click every `airflow-s` reference in the +The user has to be able to click every `` reference in the recap without manually pasting the number into the URL bar. Concrete minimum that every recap must include as clickable links: diff --git a/AGENTS.md b/AGENTS.md index 22c9b1c4..f61665c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -510,7 +510,7 @@ This applies **even when**: **Why:** every ASF project operates its own CNA process under its own security team. Content about project X's in-flight or historical vulnerability is project X's private information, not -Airflow's, and copying it into our tracker effectively re-publishes +this project's, and copying it into our tracker effectively re-publishes it via screenshots, excerpts pasted into advisories, timeline clippings, or future scrapes. Cross-project correlations also reveal investigation patterns, reporter behaviour, and triage-team @@ -527,7 +527,7 @@ channel they arrived on: - Reporter mentioned another project on the `` thread → discuss it on that same thread if it helps triage; do not copy into the tracker. -- Observation is load-bearing for Airflow's own fix or advisory +- Observation is load-bearing for our own fix or advisory (e.g. the other project's fix shape informs ours) → summarise it **without naming the project**. *"The reporter has filed similar reports with other ASF projects"* is allowed and sometimes @@ -570,13 +570,13 @@ informational background only.** Do not: The adopting project's security team scores every accepted vulnerability independently, as part of the CVE-allocation step, using the same CVSS version and vector -conventions we use for all Airflow CVEs. The independent score is the **only** +conventions for every CVE the project ships. The independent score is the **only** score that ends up in the CVE record and the public advisory. Reasons: - reporter scores are frequently inflated (*"High"* or *"Critical"* is the default for many report templates, regardless of actual exploitability in - an Airflow deployment); -- reporters typically do not know the Airflow Security Model and therefore + the project's deployment); +- reporters typically do not know the project's security model and therefore misjudge which capabilities are in-scope for a CVE in the first place; - propagating the reporter's score creates an implicit contract with them — if we later revise it downward, they feel the rug has been pulled, and the @@ -721,16 +721,16 @@ handling process, no speculation about severity or timelines beyond the single forward-looking sentence in paragraph 2. **Emails to the ASF security team are even shorter.** The ASF CVE -managers and the ASF security team already know the Airflow process, -the Vulnogram tool, and the CVE-5 schema. A message to them is a -**request or a fact**, not a briefing: +managers and the ASF security team already know the project's +process, the Vulnogram tool, and the CVE-5 schema. A message to +them is a **request or a fact**, not a briefing: - Lead with the ask or the fact in one sentence (*"Please push the attached credit correction to cve.org for CVE-YYYY-NNNNN."*). - Include only the minimum artifact the recipient needs to act (the CVE ID, the corrected JSON, the archive URL) — one link, maybe two. -- Do **not** restate the vulnerability, the Airflow release train, or - the history of the ticket. +- Do **not** restate the vulnerability, the project's release train, + or the history of the ticket. - Do **not** explain why the ASF team's action is needed when their role in the process is already established (e.g. pushing to cve.org, allocating a CVE from a PMC-gated form). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 012c3228..f01a6c40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,10 @@ Thanks for helping improve this repository. It is a reusable framework for running the ASF security-disclosure process as a set of agent-driven -skills, and today drives Apache Airflow. The same tree can be extended -to any ASF project — adding support is a matter of dropping a new -subtree under [`projects/`](projects/) and pointing -[`config/active-project.md`](config/active-project.md) at it. +skills. Adopting projects ship a per-project configuration layer +(`/`) in their own tracker repo and consume this +framework as a submodule — see [`projects/_template/`](projects/_template/) +for the scaffold an adopter copies and fills in. Before sending a patch, please skim this file end-to-end: it lays out the layering the repository depends on, and a patch that ignores the @@ -105,22 +105,16 @@ four — no hard-coded project assumptions anywhere. │ ├── user.md.template # Bootstrap template with TODOs │ └── user.md.example # Filled-in example │ -├── projects/ # One subtree per supported ASF project -│ ├── README.md # Current-projects index -│ ├── airflow/ # Active project — all Airflow-specific content -│ │ ├── project.md # Manifest: tracker repo, upstream repo, tools, -│ │ │ # board IDs, Gmail domains -│ │ ├── security-model.md -│ │ ├── canned-responses.md # Reporter-facing reply templates -│ │ ├── release-trains.md # Current cuts + release-manager roster -│ │ ├── milestones.md # Scope → milestone-format mapping -│ │ ├── scope-labels.md -│ │ ├── naming-conventions.md -│ │ ├── title-normalization.md -│ │ ├── fix-workflow.md -│ │ └── README.md -│ └── _template/ # Scaffold for bootstrapping a new project -│ └── (same shape as airflow/, with TODO placeholders) +├── projects/ # Templates for adopting projects' configs +│ └── _template/ # Scaffold for bootstrapping a new project's +│ # `/` (the per-project layer +│ # an adopter ships in their tracker repo). +│ # Files: project.md (manifest), security-model.md, +│ # canned-responses.md, release-trains.md, +│ # milestones.md, scope-labels.md, naming- +│ # conventions.md, title-normalization.md, +│ # fix-workflow.md, README.md — all stubbed +│ # with TODO placeholders. │ ├── tools/ # Project-agnostic adapters per external system │ ├── gmail/ @@ -171,7 +165,7 @@ First-time clone: ```bash git clone git@github.com:.git -cd airflow-s +cd prek install # wire the hooks into .git/hooks prek run --all-files # runs every hook on every file; does a # one-time bootstrap of config/user.md @@ -198,7 +192,7 @@ editing: |---|---| | A step of the disclosure process that applies to every project | [`README.md`](README.md) | | An editorial / confidentiality / style rule | [`AGENTS.md`](AGENTS.md) | -| Anything Airflow-specific (canned reply, milestone convention, scope label, release-train state) | [`projects/airflow/`](projects/airflow/) | +| Anything project-specific (canned reply, milestone convention, scope label, release-train state) | the adopter's own `/` (lives in their tracker repo, not here) | | An adapter surface for an external system (a new Gmail search template, a new GraphQL recipe, a new `gh` invocation, a new CVE-tool endpoint) | the matching [`tools//`](tools/) subtree | | A skill's workflow | [`.claude/skills//SKILL.md`](.claude/skills/) | | Bootstrap scaffolding for a new project | [`projects/_template/`](projects/_template/) | @@ -216,10 +210,11 @@ Rules of thumb for each layer: project (different Gmail domains, different GitHub org, different board node IDs), the adapter declares variables and the active project's [`project.md`](/project.md) fills them. -- **Project subtrees carry concrete names freely** — they exist for - exactly that. `projects/airflow/` can reference `` - directly, can paste the Keycloak provider version without - apology, can name @jscheffl as RM in `release-trains.md`. +- **An adopter's `/` carries concrete names freely** — + it exists for exactly that. The adopter's own per-project files can + reference their `` repo directly, paste concrete package + versions, name release managers, etc. — none of that lives in this + framework repo. - **Skills never mutate state without user confirmation.** If you add a new action, write the proposal/confirm/apply shape into the skill and the guardrails into `AGENTS.md`. See the existing skills for @@ -259,14 +254,13 @@ any new behaviour has to stay compatible with. ## Opening a pull request -- **Base branch:** `airflow-s`. Do not open PRs against any other - branch unless explicitly coordinated. +- **Base branch:** `main`. Do not open PRs against any other branch + unless explicitly coordinated. - **Scope:** keep one concern per PR. A skill-behaviour change, a - tool-adapter addition, and a project-content update should land as - three separate PRs. + tool-adapter addition, and a doc update should land as separate PRs. - **Commit message shape:** imperative-present subject, ≤72 chars, plain prose body explaining *why*. Look at - [recent merged commits](https://github.com//commits/airflow-s) + [recent merged commits](https://github.com/apache/airflow-steward/commits/main) for the cadence. - **PR description:** one `## Summary` section with 1–3 bullets of *what changed and why*, and one `## Test plan` section listing how diff --git a/README.md b/README.md index 5b0ca103..e6f30d67 100644 --- a/README.md +++ b/README.md @@ -55,15 +55,16 @@ - [Label lifecycle](#label-lifecycle) - [State diagram](#state-diagram) - [Label reference](#label-reference) -- [Current projects](#current-projects) +- [Adopting the framework](#adopting-the-framework) ## Overview -This repository (`apache/airflow-steward`, future-renamed to -`apache/steward`) hosts a reusable, project-agnostic framework for -running an ASF project's security-issue handling process. The +This repository hosts a reusable, project-agnostic framework for +running an ASF project's security-issue handling process. (Currently +served from `apache/airflow-steward` for legacy reasons; future- +renamed to `apache/steward`.) The lifecycle and conventions below are framework-level; everything project-specific (identity, repositories, mailing lists, canned responses, release trains, security model, scope labels, milestone @@ -118,9 +119,8 @@ Pick whichever applies to you now: - **I am new to the security team, or I mostly just want to comment on issues.** Read [Shared conventions](#shared-conventions) below. The - adopting project's security-issues board (for Airflow: - ; in general, see - `/project.md → GitHub project board`) is the main + adopting project's security-issues board — see + `/project.md → GitHub project board` — is the main view. You do not need an agent for commenting. - **I am a rotational triager** — running `import new reports` and `sync all` a few times a week. Jump to @@ -339,10 +339,12 @@ pulling at least one other security-team member into the discussion. Use the canned-response templates from [`/canned-responses.md`](/canned-responses.md) for negative assessments so the tone stays polite-but-firm. -When the report is confirmed valid, apply exactly one scope label (`airflow` -/ `providers` / `chart`). If a report affects more than one scope, split into -per-scope trackers before allocation — the `sync-security-issue` skill -surfaces this as a blocker. See +When the report is confirmed valid, apply exactly one scope label from +the project's scope set (declared in +[`/scope-labels.md`](/scope-labels.md)). +If a report affects more than one scope, split into per-scope trackers +before allocation — the `sync-security-issue` skill surfaces this as +a blocker. See [Step 5](#step-5--land-the-validinvalid-consensus). If discussion stalls for about 30 days, escalate to a broader audience per @@ -422,8 +424,8 @@ skill: - opens the public PR from your fork via `gh pr create --web` with a scrubbed title and body — every public surface (commit message, branch name, PR title, PR body, newsfragment) is grep-checked for - `CVE-`, `airflow-s`, `vulnerability`, *"security fix"* and similar - leakage before being written or pushed; + `CVE-`, the `` repo slug, `vulnerability`, *"security fix"* + and similar leakage before being written or pushed; - updates the `` tracking issue with the new PR link and applies the `pr created` label, handing back off to `sync-security-issue`. @@ -725,7 +727,7 @@ short-circuits the two best outcomes phase 1 is designed to produce: *will* miss novel approaches. - **An expert flagging that the report is invalid / out of scope.** Sometimes the right answer is "this is documented behaviour" or - "this duplicates `airflow-s#XYZ` from 2 years ago"; an AI analysis + "this duplicates `#XYZ` from 2 years ago"; an AI analysis that already proposes a fix anchors the discussion towards *implementing* that fix rather than evaluating whether one is needed. @@ -1058,19 +1060,17 @@ skill keeps these labels honest: on every run it detects the current state of the issue, the fix PR, and the release train, and proposes the label transitions the process requires. -## Current projects +## Adopting the framework -One row per project configured under -[`projects/`](projects/). The directory name is the resolution key -for the `` placeholder used throughout the framework (see -[`AGENTS.md` — Placeholder convention](AGENTS.md#placeholder-convention-used-in-skill-files)). +Projects don't live in this repository — adopters pull the framework +into their own tracker repo as a git submodule (see *Repository +purpose* in [`AGENTS.md`](AGENTS.md#repository-purpose)) and ship +their per-project configuration alongside it under +`/` (which resolves to `.apache-steward/` in the +adopter's tracker root). -| Project | Directory | Index | Manifest | -|---|---|---|---| -| [Apache Airflow](/) | [`projects/airflow/`](projects/airflow/) | [`/README.md`](/README.md) | [`/project.md`](/project.md) | - -Add a new project by copying -[`projects/_template/`](projects/_template/) into -`projects//`, filling in the TODO placeholders, and adding a -row to the table above. The full bootstrap walk-through lives in -[`projects/README.md`](projects/README.md#bootstrapping-a-new-project). +To bootstrap a new adopter, copy [`projects/_template/`](projects/_template/) +into `/` in your tracker repo, fill in the TODO +placeholders, and point the framework's skills at it via the path +resolution documented in +[`AGENTS.md` — Placeholder convention](AGENTS.md#placeholder-convention-used-in-skill-files). diff --git a/new-members-onboarding.md b/new-members-onboarding.md index 5265a578..095c2f6e 100644 --- a/new-members-onboarding.md +++ b/new-members-onboarding.md @@ -16,9 +16,9 @@ Hello, new member of the adopting project's security team. This repository hosts a project-agnostic framework for handling security -issues; the adopting project (for this clone: -[`/`](projects/airflow/)) is declared in -[`config/active-project.md`](config/active-project.md). This document +issues; the adopting project's per-project layer lives in +[`/`](/) (i.e. `.apache-steward/` at +the root of the adopter's tracker repo). This document is the soft-landing guide — it tells you how the team works, where the action happens, and what is expected of you in the first few weeks. @@ -69,23 +69,19 @@ for the lookup command and the latest snapshot. # Where things happen -- **The project's ``** (for Airflow: - ``; in general see - `/project.md → Mailing lists`). You are - subscribed automatically when you join the security team. This is - where external reporters land their reports and where we reply to - keep them informed. -- **`` GitHub repository** (for this clone: - ``) — the private tracker. Every valid report - becomes a tracking issue here. Everything that happens on an - issue is automatically mirrored to the security mailing list so - people who prefer email stay in the loop. +- **The project's ``** — see + `/project.md → Mailing lists` for the concrete + address. You are subscribed automatically when you join the + security team. This is where external reporters land their reports + and where we reply to keep them informed. +- **`` GitHub repository** — the private tracker. Every + valid report becomes a tracking issue here. Everything that + happens on an issue is automatically mirrored to the security + mailing list so people who prefer email stay in the loop. - **Security-issues board** — each project runs its own Projects V2 - board. For Airflow: - . In general, see - `/project.md → GitHub project board`. If you - want one URL to bookmark for the adopting project, bookmark that - board. + board. See `/project.md → GitHub project board` + for the URL. If you want one thing to bookmark for the adopting + project, bookmark that board. - **Private PRs on the `` `main` branch** — an exceptional path used for highly-critical fixes that need private code review before going public. See @@ -105,9 +101,8 @@ week. A good starting routine: 1. **Read [`README.md`](README.md) once, skim the role sections.** Pick the role you think you are most likely to take on first, and read it in full. -2. **Open the adopting project's board** (for Airflow: - ; in general, see - `/project.md → GitHub project board`). Get a +2. **Open the adopting project's board** — its URL is in + `/project.md → GitHub project board`. Get a feel for what states issues sit in and how they flow across columns. 3. **Subscribe to a few open issues** and read along. The mailing-list @@ -222,10 +217,10 @@ The full list of skills, and what each one does, is in Beyond reacting to inbound reports, you are also welcome to proactively look for security improvements in the adopting project. -Take a look at the tracker's Discussions tab (for Airflow: -/discussions>) — we -occasionally start a discussion there when we see we can improve -something, process-wise or tooling-wise. Join those discussions, +Take a look at the tracker's Discussions tab +(`https://github.com//discussions`) — we occasionally start +a discussion there when we see we can improve something, process-wise +or tooling-wise. Join those discussions, share your perspective, or start new ones if you see something worth fixing. @@ -238,9 +233,10 @@ shape the team are small enough to read in one sitting: - [`config/README.md`](config/README.md) — how the per-project and per-user configuration layers work, with a tutorial for your own `config/user.md`. -- [`/`](projects/airflow/) — project-specific content +- [`/`](/) — project-specific content (roster, release trains, security model, scope labels, milestones, - canned responses, fix-workflow specifics). + canned responses, fix-workflow specifics) — lives in the adopter's + tracker repo. - [`how-to-fix-a-security-issue.md`](how-to-fix-a-security-issue.md) — high-level fix workflow. - [`new-members-onboarding.md`](new-members-onboarding.md) — this diff --git a/projects/_template/README.md b/projects/_template/README.md index 8c2f174f..63c179ba 100644 --- a/projects/_template/README.md +++ b/projects/_template/README.md @@ -38,11 +38,10 @@ so a directory that starts with `_` is never accidentally picked up). ## What each file is for -Once you have copied the template and renamed the directory, update -this `README.md` to be your project's **file index**. The template -below mirrors the structure of -[`../airflow/README.md`](../airflow/README.md); delete the sections -your project does not need and fill in the rest. +Once you have copied the template into `/` in your +tracker repo, update this `README.md` to be your project's **file +index**. Delete the sections your project does not need and fill in +the rest. ### Authoritative manifest (fill this in first) @@ -102,10 +101,8 @@ your project does not need and fill in the rest. ## Cross-references -- [`../README.md`](../README.md) — framework-level *"Current - projects"* view + bootstrap walk-through. -- [`../../config/active-project.md`](../../config/active-project.md) — - the selector that picks which project under `projects/` the skills - load. -- [`../airflow/`](../airflow/) — a fully-populated example to - reference while filling in your own project. +- [`../README.md`](../README.md) — framework-level *"Adopting the + framework"* view + bootstrap walk-through. +- [`../../AGENTS.md`](../../AGENTS.md#placeholder-convention-used-in-skill-files) — + the placeholder convention that lets skills resolve `/` + to the adopter's path at agent runtime. diff --git a/projects/_template/canned-responses.md b/projects/_template/canned-responses.md index a56ffed8..1c6d52b0 100644 --- a/projects/_template/canned-responses.md +++ b/projects/_template/canned-responses.md @@ -48,9 +48,8 @@ canonicalise. ## TODO — minimum viable canned responses At a minimum, the following templates should exist before a tracker -team goes live. Copy one of the shapes from -[`../airflow/canned-responses.md`](../airflow/canned-responses.md) -and adapt the wording to the project. +team goes live. The shapes are stubbed below; adapt the wording to +the adopting project's voice. ### Confirmation of receiving the report @@ -116,9 +115,8 @@ Minimum set (one per lifecycle transition): | CVE published on cve.org | post-Step 15 | `sync-security-issue` skill (recently-closed scan) | | Credit correction | Step 16 | Release manager | -See -[`../airflow/canned-responses.md`](../airflow/canned-responses.md) -for fully-populated worked examples to adapt. +Each row above corresponds to a section below; flesh out the +template body with project-specific wording. ### CVE allocated (Step 6) diff --git a/projects/_template/release-trains.md b/projects/_template/release-trains.md index 4e05533f..0fbdec5d 100644 --- a/projects/_template/release-trains.md +++ b/projects/_template/release-trains.md @@ -29,7 +29,7 @@ TODO: list the project's active release branches. For each branch: - whether new security fixes should default to this branch or a different one. -Example shape (from `projects/airflow/release-trains.md`): +Example shape: > - **`main`** — becomes the next minor release (X.Y+1.0 eventually). > - **`v1-2-test`** — patch branch for the `1.2.x` series. Next patch is `1.2.3`. diff --git a/projects/_template/title-normalization.md b/projects/_template/title-normalization.md index 886ffd27..722bac5e 100644 --- a/projects/_template/title-normalization.md +++ b/projects/_template/title-normalization.md @@ -52,9 +52,18 @@ TODO: one rule per bullet, applied in order. Typical patterns: ## Implementation recipe TODO: keep the transform inline in the skill, do not create a -separate Python project. See -[`../airflow/title-normalization.md`](../airflow/title-normalization.md#implementation-recipe) -for a worked example (Airflow's cascade). +separate Python project. A typical cascade looks like: + +1. Strip a leading `[ Security Report ]` or similar harness prefix. +2. Strip a leading `: :` (e.g. the project's own + "Apache Foo:" prefix that the CVE tool re-applies). +3. Strip a trailing version-parenthetical like `(<= 1.2.3)`. +4. Strip a leading `Re:` if the original report came in by email and + was retitled with the reply prefix. + +The result is the bare vulnerability description that goes into the +CVE record's `title` field. Document the cascade your project uses +in this file once you settle on it. ## Sanity check diff --git a/pyproject.toml b/pyproject.toml index 7974eb07..6b319222 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,12 @@ # own pyproject.toml + uv.lock. [project] -name = "apache-airflow-steward" +# Forward-looking name: the framework lives at `apache/airflow-steward` +# today (legacy from when it was Airflow's private security tracker) +# and is renaming to `apache/steward`. The package name here matches +# the future canonical name; no PyPI publication, so the name divergence +# from the GitHub slug is harmless. +name = "apache-steward" version = "0.0.0" description = "Reusable framework for handling security vulnerabilities in Apache projects." requires-python = ">=3.11" diff --git a/tools/github/project-board.md b/tools/github/project-board.md index d90a3f78..dff10174 100644 --- a/tools/github/project-board.md +++ b/tools/github/project-board.md @@ -67,7 +67,7 @@ intended behaviour, since such issues are usually noise. neither `gh` nor a GraphQL mutation can set it. To change it: 1. Open `/workflows` (for Airflow: - ). + </workflows>). 2. Click **Auto-add to project**. 3. Edit the **Filter** field to the expression above. 4. Save. diff --git a/tools/github/tool.md b/tools/github/tool.md index 0bdbf67e..83a5c400 100644 --- a/tools/github/tool.md +++ b/tools/github/tool.md @@ -64,7 +64,7 @@ when the tool changes. ## Confidentiality note Some of the recipes below operate on the **private** tracker repo -(e.g. `` for the Airflow project) and others on a +(e.g. `` for the adopting project) and others on a public repo (e.g. ``). The confidentiality rules in [`../../AGENTS.md`](../../AGENTS.md) still bind regardless of which tool is in use: anything that lands on a public surface must be diff --git a/tools/gmail/asf-relay.md b/tools/gmail/asf-relay.md index 5abcc3f2..d8fc2bf3 100644 --- a/tools/gmail/asf-relay.md +++ b/tools/gmail/asf-relay.md @@ -30,9 +30,9 @@ the differences are in the headers and body shape. Placeholder convention: -- `` — the project's security list. For Airflow, - ``; see - [`../..//project.md`](../..//project.md#mailing-lists). +- `` — the project's security list. The concrete + address is declared in + [`/project.md → Mailing lists`](../..//project.md#mailing-lists). ## Rules diff --git a/tools/gmail/draft-backends.md b/tools/gmail/draft-backends.md index dab4fd5b..2923dcb2 100644 --- a/tools/gmail/draft-backends.md +++ b/tools/gmail/draft-backends.md @@ -172,7 +172,7 @@ to the user before drafting a new one"*. ## Known issue — `oauth_curl` thread-attached drafts may not surface in the global Drafts folder -Caught live on 2026-04-25 during the [`airflow-s#346`](https://github.com//issues/346) +Caught live on 2026-04-25 during the [`#346`](https://github.com//issues/346) fix-skill flow: when **multiple `oauth_curl`-backed drafts pile up on the same Gmail thread** within a single skill flow (typical sequence: allocate-cve drafts a CVE-allocated message → sync-security-issue diff --git a/tools/gmail/operations.md b/tools/gmail/operations.md index 44e43e3d..db2e4790 100644 --- a/tools/gmail/operations.md +++ b/tools/gmail/operations.md @@ -219,7 +219,7 @@ to that user until sent. Draft content may reference the private tracker's URL (reporter is on the private thread and is expected to keep it confidential), but anything destined for a public list must obey the confidentiality rules in -[`../../AGENTS.md`](../../AGENTS.md) — no `airflow-s` URLs, no CVE +[`../../AGENTS.md`](../../AGENTS.md) — no `` URLs, no CVE IDs before publication, no *"security fix"* leakage. ## Error handling diff --git a/tools/gmail/search-queries.md b/tools/gmail/search-queries.md index b777ea41..e56ec62b 100644 --- a/tools/gmail/search-queries.md +++ b/tools/gmail/search-queries.md @@ -60,7 +60,7 @@ scan excludes the mirror senders up front: ```text -from:notifications@github.com -from:noreply@github.com --from:airflow-s@noreply.github.com +-from: -from:security-noreply@github.com ``` @@ -78,7 +78,7 @@ bots, within a time window: list: -from:notifications@github.com -from:noreply@github.com - -from:airflow-s@noreply.github.com + -from: -from:security-noreply@github.com newer_than:30d ``` @@ -122,7 +122,7 @@ distinctive noun-phrase set — the same 3–5 tokens the skill's Step down to messages whose sender is on the security-team roster AND whose body opens with a canned-response preamble (*"Thank you for reporting … this isn't a security issue"*, -*"Per the Airflow Security Model"*, *"This is expected behaviour +*"Per the project's security model"*, *"This is expected behaviour for a Dag author"*, *"We treat this as out of scope of the Security Model"*). Those are the prior-rejection precedents. @@ -180,14 +180,14 @@ The tool's JSON API is OAuth-gated and not readable from skill context, so Gmail is the load-bearing signal path. ```text - -from:notifications@github.com -from:noreply@github.com -from:airflow-s@noreply.github.com list: + -from:notifications@github.com -from:noreply@github.com -from: list: ``` A second search without the `list:` filter catches CNA-tooling emails that went to individual security-team members first: ```text - -from:notifications@github.com -from:noreply@github.com -from:airflow-s@noreply.github.com + -from:notifications@github.com -from:noreply@github.com -from: ``` Sync-style skills impose a hard per-issue Gmail-call budget. Keep diff --git a/tools/ponymail/tool.md b/tools/ponymail/tool.md index 0cec818c..42893aba 100644 --- a/tools/ponymail/tool.md +++ b/tools/ponymail/tool.md @@ -95,8 +95,8 @@ Prerequisites: - Python 3.11+ (the MCP server is a Python package). - An ASF LDAP account with access to the lists the project needs. - For the Airflow security team, that is the `pmc-airflow` LDAP - group, which gates the `` and + Typically that is the project's PMC LDAP group (e.g. + `pmc-`), which gates the `` and `` archives. - A local browser that can complete the ASF OAuth redirect flow (the login tool opens a browser window for LDAP authentication). @@ -181,8 +181,8 @@ lists: mcp__ponymail__list_lists() ``` -The result is a `{ domain → { list → message_count } }` map. For an -Airflow security-team triager, you should see +The result is a `{ domain → { list → message_count } }` map. For a +PMC-LDAP-authenticated triager, you should see the project's `` → `{ security: }` — proof that the session has PMC-level LDAP access. If you only see public lists (`dev`, `users`, `announce`), the LDAP group membership is not being diff --git a/tools/vulnogram/generate-cve-json/SKILL.md b/tools/vulnogram/generate-cve-json/SKILL.md index cbc8275b..a854bcd8 100644 --- a/tools/vulnogram/generate-cve-json/SKILL.md +++ b/tools/vulnogram/generate-cve-json/SKILL.md @@ -28,14 +28,14 @@ to eliminate the manual "copy each field from the issue into the right Vulnogram form input" step when you are preparing to publish an advisory. > **Project-agnostic by design.** All project-specific values -> (vendor, top-level product / package name, provider display map, +> (vendor, top-level product / package name, project display map, > CNA org id, generator tag, …) are loaded from a TOML config the > adopting project ships at `/tools/vulnogram/cve-json-config.toml`. -> Examples in this document use Apache Airflow's configuration as a -> running illustration, since that's where the tool was originally -> developed; any adopter writes their own config per the schema in -> the package [README](README.md) and the tool emits CVE records -> against their own product taxonomy. +> Concrete `apache-foo-project-*` strings appearing in this +> document are **illustrative examples** of how a project with a +> project-style package layout would configure things; replace +> them mentally with the adopter's own package taxonomy. The +> schema is documented in the package [README](README.md). **Golden rule:** the script generates a *proposal* JSON document. It parses a handful of structured fields from the issue body, but it cannot @@ -59,9 +59,10 @@ diff the two to see what the tool has added / what you changed by hand. - `--cve-id CVE-YYYY-NNNN+` — override the CVE ID if the issue body's `CVE tool link` field has not yet been filled in, or retarget the JSON to a different CVE ID. - - `--title "Apache Airflow: …"` — override the CVE title. Default is - the GitHub issue title with an `: :` (e.g. `Apache Airflow:`) prefix when it - does not already start with that phrase. + - `--title ": : …"` — override the CVE title. + Default is the GitHub issue title with the project's + `: :` prefix (sourced from the TOML config) when + it does not already start with that phrase. - `--version-start X.Y.Z` — override the start of the affected version range (the `affected[].versions[].version` field). Default is the lower bound parsed from the Affected versions field when it uses @@ -75,18 +76,18 @@ diff the two to see what the tool has added / what you changed by hand. credit(s) from the *Reporter credited as* field are always emitted with `type: "finder"`. - `--vendor` / `--product` / `--package-name` / `--collection-url` — - override the product identity fields (defaults to - `"Apache Software Foundation"` / `"Apache Airflow"` / - `"apache-airflow"` / `"https://pypi.python.org"`). These are - used as the default identity for *Affected versions* lines that - don't start with a recognisable Airflow package directory name + override the product identity fields. The defaults come from the + project's TOML config (`product.vendor`, `product.default_product`, + `product.default_package_name`, `product.default_collection_url`). + They are used as the identity for *Affected versions* lines that + don't start with a recognisable per-package directory name (see the multi-product note below). - `--product-for PACKAGE=PRODUCT` — override the CVE product display name for a specific `packageName`. Repeat to override multiple - packages. Useful when a provider is not yet in the built-in - display-name map, or when an acronym needs different casing from - the title-cased fallback. Example: - `--product-for apache-airflow-providers-foo='Apache Airflow Providers Foo'`. + packages. Useful when a package is not in the project's + `project_display_map` config, or when an acronym needs different + casing from the title-cased fallback. Example: + `--product-for apache-foo-project-baz='Apache Foo Project Baz'`. - `--org-id ` — override the CNA assigner org id (defaults to the ASF org id). - `--discovery ` — override `source.discovery` (default @@ -121,30 +122,31 @@ filled in through a prior `sync-security-issue` run. In particular: `>= 2.0.0, < 3.2.2`, `<= 3.2.1`, a bare version like `3.1.5`, and a bare lower bound like `>= 2.0.0`). **Multi-product CVEs are supported** — put one package per line, - prefixing each with the Airflow package directory name, and the - script emits one `affected[]` entry per line with the right - `product` / `packageName`. Example: - - apache-airflow-providers-elasticsearch <=6.5.0 - apache-airflow-providers-opensearch <=1.9.0 - - Known provider directory names (`elasticsearch`, `opensearch`, - `cncf-kubernetes`, `smtp`, …) are resolved to the vendor-preferred - display casing via the built-in `AIRFLOW_PROVIDER_DISPLAY_MAP`; - unknown providers fall back to title-cased dash-split and can be - overridden with `--product-for`. A line without a package prefix - (or a single-line field) falls back to the `--product` / - `--package-name` defaults, which preserves the historical + prefixing each with the package directory name as it appears in + the adopter's repo, and the script emits one `affected[]` entry + per line with the right `product` / `packageName`. Example + (illustrative — using a hypothetical `apache-foo` project's + sub-project layout): + + apache-foo-project-alpha <=6.5.0 + apache-foo-project-beta <=1.9.0 + + Known package directory names are resolved to the vendor-preferred + display casing via the project's `packages.project_display_map` + config table; unknown packages fall back to title-cased dash-split + and can be overridden with `--product-for`. A line without a + package prefix (or a single-line field) falls back to the + `--product` / `--package-name` defaults, which preserves the single-product behaviour. - **`< NEXT VERSION` placeholder** — providers trackers don't know which - package version will ship the fix until the wave's release manager - picks it during the `Providers YYYY-MM-DD` cut. Until then, the + **`< NEXT VERSION` placeholder** — multi-package trackers don't + know which package version will ship the fix until the wave's + release manager picks it during a release cut. Until then, the *Affected versions* lines use the literal token `NEXT VERSION` as the upper bound, e.g.: - apache-airflow-providers-elasticsearch < NEXT VERSION - apache-airflow-providers-opensearch < NEXT VERSION + apache-foo-project-alpha < NEXT VERSION + apache-foo-project-beta < NEXT VERSION The generator strips `< NEXT VERSION` before parsing each line and emits a `versions[]` entry without `lessThan` (open-ended upper @@ -225,8 +227,8 @@ in `README.md`. Before reading the tracker: -1. `gh api repos/ --jq .name` returns - `airflow-s`, **and** +1. `gh api repos/ --jq .name` returns the adopter's + tracker repo name (per `/project.md`), **and** 2. `uv --version` returns. If either fails, stop and tell the user what to install or log @@ -287,9 +289,10 @@ Additional flags, all optional: - `--cve-id CVE-YYYY-NNNN+` — override the CVE ID if the *CVE tool link* field is empty. -- `--title "Apache Airflow: …"` — override the title. +- `--title ": : …"` — override the title. - `--vendor` / `--product` / `--package-name` / `--collection-url` — - override product identity (defaults cover Apache Airflow on PyPI). + override product identity (defaults sourced from the project's TOML + config under `[product]`). - `--org-id ` — override the CNA assigner org id (defaults to the ASF org id). - `--discovery UNKNOWN|INTERNAL|EXTERNAL|USER` — override @@ -469,17 +472,17 @@ tool-side difference is intentional and the reviewer will keep it. ## Guardrails - **Confidentiality.** The script deliberately **drops** any URL that - points at `cveprocess.apache.org` or `airflow-s` from the references - list before serialising. Those URLs are private ASF-internal tracker - links and should not appear in a published CVE record. See the - "Confidentiality of ``" section of + points at `cveprocess.apache.org` or the project's `` repo + from the references list before serialising. Those URLs are private + ASF-internal links and should not appear in a published CVE record. + See the "Confidentiality of ``" section of [`AGENTS.md`](../../../AGENTS.md). - **CVE IDs are always linked** per the "Linking CVEs" rule in [`AGENTS.md`](../../../AGENTS.md). When the skill mentions the CVE in - proposals, recaps or comments on the airflow-s issue, it must render - the ID as a markdown link — before publication to the ASF CVE tool, and - additionally to `cve.org` after publication. -- **Airflow-s references are always linked** per the "Linking + proposals, recaps or comments on the `` issue, it must + render the ID as a markdown link — before publication to the ASF + CVE tool, and additionally to `cve.org` after publication. +- **`` references are always linked** per the "Linking `` issues and PRs" rule in [`AGENTS.md`](../../../AGENTS.md). When the skill mentions the tracking issue in its own comments, render it as a markdown link. diff --git a/tools/vulnogram/generate-cve-json/src/generate_cve_json/__init__.py b/tools/vulnogram/generate-cve-json/src/generate_cve_json/__init__.py index f304b45b..ea8d440c 100644 --- a/tools/vulnogram/generate-cve-json/src/generate_cve_json/__init__.py +++ b/tools/vulnogram/generate-cve-json/src/generate_cve_json/__init__.py @@ -27,7 +27,7 @@ from generate_cve_json.cve_json import ( COLLECTION_URL_TO_PROJECT_URL_TEMPLATE, NEXT_VERSION_TOKEN_RE, - PROVIDER_DISPLAY_MAP, + PROJECT_DISPLAY_MAP, _build_attachment_body, _has_vendor_advisory_reference, _is_cna_ready_for_review, @@ -62,7 +62,7 @@ __all__ = [ "COLLECTION_URL_TO_PROJECT_URL_TEMPLATE", "NEXT_VERSION_TOKEN_RE", - "PROVIDER_DISPLAY_MAP", + "PROJECT_DISPLAY_MAP", "_build_attachment_body", "_has_vendor_advisory_reference", "_is_cna_ready_for_review", diff --git a/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py b/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py index a5386c3e..9675f340 100644 --- a/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py +++ b/tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py @@ -101,7 +101,7 @@ # Configuration loading. # ----------------------------------------------------------------------------- # -# All project-specific values (vendor, top-level product/package, provider +# All project-specific values (vendor, top-level product/package, project # display map, package-name regex, CNA org id, generator tag, …) are read # at module load time from a TOML config file the adopting project ships # in its tracker repo at: @@ -174,9 +174,11 @@ def _populate_constants() -> None: global DEFAULT_REPO, DEFAULT_VENDOR, DEFAULT_PRODUCT global DEFAULT_PACKAGE_NAME, DEFAULT_COLLECTION_URL global DEFAULT_ASF_ORG_ID, GENERATOR_TAG, SKILL_SOURCE_URL - global PROVIDER_DISPLAY_MAP, PACKAGE_RE - global TOP_LEVEL_NAME, TOP_LEVEL_PRODUCT, PROVIDER_PRODUCT_TEMPLATE - global PROVIDER_PREFIX + global PROJECT_DISPLAY_MAP, PACKAGE_RE + global TOP_LEVEL_NAME, TOP_LEVEL_PRODUCT, PROJECT_PRODUCT_TEMPLATE + global PROJECT_PREFIX + global CNA_PRIVATE_PROJECT_URL, CNA_PRIVATE_OWNER, CNA_PRIVATE_USERS_LIST + global TITLE_STRIP_RE, TRACKER_FILTER_TOKEN DEFAULT_REPO = cfg["meta"]["tracker_repo"] DEFAULT_VENDOR = cfg["product"]["vendor"] @@ -188,14 +190,40 @@ def _populate_constants() -> None: SKILL_SOURCE_URL = cfg["meta"].get( "skill_source_url", f"https://github.com/{cfg['meta']['tracker_repo']}" ) - PROVIDER_DISPLAY_MAP = dict(cfg["packages"]["provider_display_map"]) + PROJECT_DISPLAY_MAP = dict(cfg["packages"]["project_display_map"]) PACKAGE_RE = re.compile(cfg["packages"]["package_pattern"]) TOP_LEVEL_NAME = cfg["packages"]["top_level_name"] TOP_LEVEL_PRODUCT = cfg["packages"]["top_level_product"] - PROVIDER_PRODUCT_TEMPLATE = cfg["packages"]["provider_product_template"] + PROJECT_PRODUCT_TEMPLATE = cfg["packages"]["project_product_template"] # Convenient derived constant: the prefix used to detect - # "provider" subpackages (e.g. ``apache-airflow-providers-``). - PROVIDER_PREFIX = f"{TOP_LEVEL_NAME}-providers-" + # "project" subpackages (e.g. ``-project-``). + PROJECT_PREFIX = f"{TOP_LEVEL_NAME}-project-" + + # Per-project CVE 5.x `CNA_private` envelope fields. These end up + # in every CVE record this tool generates for the project, and + # used to be hardcoded to Airflow values — now config-driven so + # adopters can populate them with their project's URLs / lists. + cna_private_cfg = cfg.get("cna_private", {}) + CNA_PRIVATE_PROJECT_URL = cna_private_cfg.get("project_url", "") + CNA_PRIVATE_OWNER = cna_private_cfg.get("owner", "") + CNA_PRIVATE_USERS_LIST = cna_private_cfg.get("users_list", "") + + # Title-strip regex for `resolve_title` — built from the configured + # top-level product so the *": :"* prefix is + # stripped without hardcoding a specific project's name. + _stripped_product = re.escape(TOP_LEVEL_PRODUCT.strip()) + TITLE_STRIP_RE = re.compile( + rf"^\s*{_stripped_product}\s*[:\-–—]?\s*", # noqa: RUF001 — en-/em-dash deliberate + re.IGNORECASE, + ) + + # Substring used by `build_references` to filter out tracker URLs + # before serialising. We use the tracker repo's owner segment + # (the part before `/`) — distinctive enough for any URL that + # points at the tracker (issues, PRs, raw blobs all carry it) + # without false positives on URLs that mention the project name + # in a path component. + TRACKER_FILTER_TOKEN = DEFAULT_REPO.split("/", 1)[0] # Module-level constants — populated by `_populate_constants()`. The @@ -210,12 +238,17 @@ def _populate_constants() -> None: DEFAULT_ASF_ORG_ID: str = "" GENERATOR_TAG: str = "" SKILL_SOURCE_URL: str = "" -PROVIDER_DISPLAY_MAP: dict[str, str] = {} +PROJECT_DISPLAY_MAP: dict[str, str] = {} PACKAGE_RE: re.Pattern[str] = re.compile("") TOP_LEVEL_NAME: str = "" TOP_LEVEL_PRODUCT: str = "" -PROVIDER_PRODUCT_TEMPLATE: str = "" -PROVIDER_PREFIX: str = "" +PROJECT_PRODUCT_TEMPLATE: str = "" +PROJECT_PREFIX: str = "" +CNA_PRIVATE_PROJECT_URL: str = "" +CNA_PRIVATE_OWNER: str = "" +CNA_PRIVATE_USERS_LIST: str = "" +TITLE_STRIP_RE: re.Pattern[str] = re.compile("") +TRACKER_FILTER_TOKEN: str = "" # CVE 5.x convention values that are not project-specific. DEFAULT_CREDIT_TYPE = "finder" @@ -231,9 +264,9 @@ def _populate_constants() -> None: NO_RESPONSE = "_No response_" # Sentinel token for an as-yet-unreleased upper bound in -# ``Affected versions`` entries. Used predominantly for providers -# trackers where the wave milestone (date-based, e.g. ``Providers -# 2026-04-21``) does not reveal which exact provider package version +# ``Affected versions`` entries. Used predominantly for projects +# trackers where the wave milestone (date-based, e.g. ``Projects +# 2026-04-21``) does not reveal which exact project package version # will ship the fix. The token is stripped before parsing and the # resulting CVE 5.x ``versions[]`` entry omits ``lessThan`` — # downstream consumers (Vulnogram, cve.org) read that as @@ -265,7 +298,6 @@ def _populate_constants() -> None: # Back-compat alias — the `v1` comment-based attachments used a single # marker. New body-embedded attachments use a begin/end pair. ATTACHMENT_MARKER_PREFIX = ATTACHMENT_MARKER_BEGIN_PREFIX -SKILL_SOURCE_URL = "https://github.com/airflow-s/airflow-s/tree/airflow-s/tools/vulnogram/generate-cve-json" # ----------------------------------------------------------------------------- @@ -441,7 +473,7 @@ def parse_affected_versions(value: str, version_start_override: str | None) -> l whose fix-shipped version is not yet known). The ``< NEXT VERSION`` sentinel signals "fix not yet released, - upper bound unknown" — used predominantly on providers trackers + upper bound unknown" — used predominantly on project trackers where the wave milestone is date-based and the package version that will carry the fix is decided by the release manager during the wave. The token is stripped before further parsing; the @@ -613,7 +645,9 @@ def build_references( gathered: list[str] = list(parse_url_list(pr_field)) if extra_urls: gathered.extend(extra_urls) - filtered = [url for url in gathered if "cveprocess.apache.org" not in url and "airflow-s" not in url] + filtered = [ + url for url in gathered if "cveprocess.apache.org" not in url and TRACKER_FILTER_TOKEN not in url + ] # De-duplicate preserving order, then sort alphabetically. seen: set[str] = set() ordered: list[str] = [] @@ -649,16 +683,16 @@ def _product_for_package( * `package_name == TOP_LEVEL_NAME` → `TOP_LEVEL_PRODUCT` (e.g. `apache-airflow` → `Apache Airflow`). - * `package_name` matches `-providers-` → - `PROVIDER_PRODUCT_TEMPLATE.format(display=...)`, where `display` - is `PROVIDER_DISPLAY_MAP[]` when the directory name is + * `package_name` matches `-project-` → + `PROJECT_PRODUCT_TEMPLATE.format(display=...)`, where `display` + is `PROJECT_DISPLAY_MAP[]` when the directory name is known, or a title-cased dash-split fallback otherwise (`foo-bar` → `Foo Bar`). * `product_overrides` lets callers shadow either source by package name — used by the `--product-for` CLI flag for unknown subpackages or acronyms that don't round-trip through `title()`. - Unknown `package_name` (neither `TOP_LEVEL_NAME` nor a provider + Unknown `package_name` (neither `TOP_LEVEL_NAME` nor a project subpackage) is returned unchanged — callers that pass a non-project package name are opting into bring-your-own product naming. @@ -670,13 +704,13 @@ def _product_for_package( return overrides[package_name] if package_name == TOP_LEVEL_NAME: return TOP_LEVEL_PRODUCT - if package_name.startswith(PROVIDER_PREFIX): - provider_dir = package_name[len(PROVIDER_PREFIX) :] - if provider_dir in PROVIDER_DISPLAY_MAP: - display = PROVIDER_DISPLAY_MAP[provider_dir] + if package_name.startswith(PROJECT_PREFIX): + project_dir = package_name[len(PROJECT_PREFIX) :] + if project_dir in PROJECT_DISPLAY_MAP: + display = PROJECT_DISPLAY_MAP[project_dir] else: - display = " ".join(part.title() for part in provider_dir.split("-") if part) - return PROVIDER_PRODUCT_TEMPLATE.format(display=display) + display = " ".join(part.title() for part in project_dir.split("-") if part) + return PROJECT_PRODUCT_TEMPLATE.format(display=display) return package_name @@ -685,7 +719,7 @@ def _split_affected_lines(affected_versions_value: str) -> list[tuple[str | None Each non-empty line is inspected for a leading Airflow package directory name (``apache-airflow`` or - ``apache-airflow-providers-``); when detected, the line splits + ``apache-airflow-project-``); when detected, the line splits into ``(package_name, version_expression)``. Lines without a recognisable package prefix yield ``(None, )`` so the caller can fall back to the default product/package. Bullets @@ -734,14 +768,14 @@ def build_affected( The *Affected versions* body field is parsed line by line. Each line that starts with an Airflow package directory name - (``apache-airflow`` or ``apache-airflow-providers-``) creates + (``apache-airflow`` or ``apache-airflow-project-``) creates an entry with that package's derived product / packageName. Lines without a recognisable prefix — or an empty field — fall back to the explicit ``product`` / ``package_name`` arguments, which is the historical single-entry shape. ``product_overrides`` is consulted *before* - :data:`PROVIDER_DISPLAY_MAP` and lets callers shadow the + :data:`PROJECT_DISPLAY_MAP` and lets callers shadow the resolved product name for any specific package. It is wired to the ``--product-for PACKAGE=PRODUCT`` CLI flag. """ @@ -1066,9 +1100,9 @@ def wrap_cve_record(cna: dict, *, cve_id: str, org_id: str) -> dict: record: dict = { "CNA_private": { "emailed": None, - "projecturl": "https://airflow.apache.org/", - "owner": "airflow", - "userslist": "users@airflow.apache.org", + "projecturl": CNA_PRIVATE_PROJECT_URL, + "owner": CNA_PRIVATE_OWNER, + "userslist": CNA_PRIVATE_USERS_LIST, "state": compute_cna_private_state(cna, cve_id), "todo": [], "type": "unsure", @@ -1475,8 +1509,8 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: metavar="PACKAGE=PRODUCT", help=( "Override the CVE product name for a specific packageName, " - "e.g. `--product-for apache-airflow-providers-foo='Apache " - "Airflow Foo Provider'`. Useful when a provider is not yet " + "e.g. `--product-for apache-airflow-project-foo='Apache " + "Airflow Foo Project'`. Useful when a project is not yet " "in the built-in display-name map, or when the default " "title-cased fallback needs a different casing. Repeat the " "flag to override multiple packages." @@ -1581,15 +1615,13 @@ def resolve_title( title = issue_title.strip() if not title: title = summary.split(".", 1)[0].strip() if summary else "" - # The CNA container is already scoped to Apache/Airflow, so the - # repeated "Apache Airflow:" prefix on the title is noise. Strip it - # (along with any trailing punctuation/whitespace) if present. - title = re.sub( - r"^\s*apache\s+airflow\s*[:\-–—]?\s*", # noqa: RUF001 — en-dash/em-dash are deliberate regex matches - "", - title, - flags=re.IGNORECASE, - ) + # The CNA container is already scoped to the project, so the + # repeated ": :" prefix on the title is noise. + # Strip it (along with any trailing punctuation/whitespace) if + # present. The strip pattern is built from the configured + # `top_level_product` at config-load time — see `TITLE_STRIP_RE` + # in `_populate_constants`. + title = TITLE_STRIP_RE.sub("", title) return title diff --git a/tools/vulnogram/generate-cve-json/tests/fixtures/cve-json-config.toml b/tools/vulnogram/generate-cve-json/tests/fixtures/cve-json-config.toml index b804b945..ec9f83a0 100644 --- a/tools/vulnogram/generate-cve-json/tests/fixtures/cve-json-config.toml +++ b/tools/vulnogram/generate-cve-json/tests/fixtures/cve-json-config.toml @@ -17,163 +17,97 @@ # generate-cve-json — TEST FIXTURE config. # -# This file is a fixture for the tool's own pytest suite, NOT the -# framework's default configuration. It carries values that match the -# Apache Airflow security team's adopter-side config because the -# tests in `test_generate_cve_json.py` were written against that -# specific configuration shape (Airflow-specific package names, -# provider display map, etc.). +# This file is a fixture for the tool's own pytest suite, NOT a +# default that adopters should copy. It describes a fictional +# **`apache-example`** project — top-level package `apache-example` +# with a sub-project layout (`apache-example-project-`). The +# shape is realistic enough to exercise every code path in the tool +# (top-level + sub-project package matching, project display-map +# lookup, multi-product CVE handling, the `< NEXT VERSION` +# placeholder, etc.) without tying the test suite to any real +# adopter's taxonomy. # -# Adopting projects MUST NOT copy this file. Write your own +# Adopters MUST NOT copy this file. Write your own # `cve-json-config.toml` from scratch using the schema documented in -# the package README. The values here are illustrative of one -# specific adopter's configuration; nothing in the framework treats -# them as defaults. +# the package README. Nothing in the framework treats the values +# below as defaults. [product] -# CVE 5.x `affected[].vendor` — the CNA vendor name. +# CVE 5.x `affected[].vendor` — the CNA vendor name. All Apache +# project CVEs share this vendor regardless of which project ships +# the affected code. vendor = "Apache Software Foundation" # Top-level product display name and package name. Used as the default # `product` / `packageName` for *Affected versions* lines that don't # match a more specific entry in `[packages]` below. -default_product = "Apache Airflow" -default_package_name = "apache-airflow" +default_product = "Apache Example" +default_package_name = "apache-example" # Where the package is distributed (`affected[].collectionURL`). default_collection_url = "https://pypi.python.org" [cna] # CNA assigner UUID (CVE 5.x `cveMetadata.assignerOrgId` / -# `providerMetadata.orgId`). This is the ASF org id. +# `providerMetadata.orgId`). This is the ASF org id (shared across +# every Apache project's CVEs). org_id = "f0158376-9dc2-43b6-827c-5f631a4d8d09" +[cna_private] +# Per-project fields that go into the CVE 5.x `CNA_private` +# envelope. These are project-scoped values that ASF Vulnogram +# uses internally; adopters fill them with their own URLs. +project_url = "https://example.apache.org/" +owner = "example" +users_list = "users@example.apache.org" + [meta] # Tracker repo slug (org/name). Used for the `x_generator.engine` # tag in the CVE record and for the self-source-link below. -tracker_repo = "airflow-s/airflow-s" +tracker_repo = "apache-example-s/apache-example-s" # Stable identifier embedded in the CVE record's `x_generator.engine` # field so a reader can identify which tool produced the JSON. -generator_tag = "airflow-s/generate_cve_json.py" +generator_tag = "apache-example-s/generate_cve_json.py" # Self-source URL the script can self-document with (e.g. in error # messages, on the `_print_skill_link` line of the generated comment). # Optional: defaults to f"https://github.com/{tracker_repo}". -skill_source_url = "https://github.com/airflow-s/airflow-s/tree/airflow-s/tools/vulnogram/generate-cve-json" +skill_source_url = "https://github.com/apache-example-s/apache-example-s/tree/main/tools/vulnogram/generate-cve-json" [packages] # Regex matching this project's package names. Required named groups: -# `package` — the full package name as shipped on PyPI (whatever -# collection_url declares). -# `provider` — optional; the subpackage / provider directory name -# used to look up a display name in -# `provider_display_map` below. May be empty when the -# line names the top-level package only. -# `rest` — optional; everything after the package name, used -# by the script to consume the trailing version-range -# expression. -package_pattern = '^(?Papache-airflow(?:-providers-(?P[a-z0-9][a-z0-9_-]*))?)(?:\s+(?P.*))?$' +# `package` — the full package name as shipped on PyPI (whatever +# collection_url declares). +# `project` — optional; the sub-project directory name used to look +# up a display name in `project_display_map` below. May +# be empty when the line names the top-level package +# only. +# `rest` — optional; everything after the package name, used +# by the script to consume the trailing version-range +# expression. +package_pattern = '^(?Papache-example(?:-project-(?P[a-z0-9][a-z0-9_-]*))?)(?:\s+(?P.*))?$' # Top-level package name + product. When the matched `package_pattern` -# yields just the top-level name (no `provider` group), the product is -# `top_level_product`. When `provider` is present, the product is -# `provider_product_template` with `{display}` substituted from -# `provider_display_map` (or a title-cased fallback). -top_level_name = "apache-airflow" -top_level_product = "Apache Airflow" -provider_product_template = "Apache Airflow Providers {display}" +# yields just the top-level name (no `project` group), the product is +# `top_level_product`. When `project` is present, the product is +# `project_product_template` with `{display}` substituted from +# `project_display_map` (or a title-cased fallback). +top_level_name = "apache-example" +top_level_product = "Apache Example" +project_product_template = "Apache Example Project {display}" -# Provider directory-name → vendor-preferred display casing for the -# CVE `product` field. Provider directory names are lowercase -# (`elasticsearch`, `cncf-kubernetes`); CVE product names follow -# vendor-preferred casing (`Elasticsearch`, `CNCF Kubernetes`). -# Extend this table when a new provider appears in a CVE; unknown -# providers fall back to a title-cased dash-split of the directory -# name, which is correct for most single-word providers but may need -# an entry here for acronyms. -[packages.provider_display_map] -"cncf-kubernetes" = "CNCF Kubernetes" -"elasticsearch" = "Elasticsearch" -"opensearch" = "OpenSearch" -"smtp" = "SMTP" -"ssh" = "SSH" -"ftp" = "FTP" -"http" = "HTTP" -"imap" = "IMAP" -"openfaas" = "OpenFaaS" -"openlineage" = "OpenLineage" -"mysql" = "MySQL" -"postgres" = "PostgreSQL" -"sqlite" = "SQLite" -"odbc" = "ODBC" -"jdbc" = "JDBC" -"sftp" = "SFTP" -"amazon" = "Amazon" -"google" = "Google" -"microsoft-azure" = "Microsoft Azure" -"microsoft-mssql" = "Microsoft SQL Server" -"microsoft-winrm" = "Microsoft WinRM" -"apache-beam" = "Apache Beam" -"apache-cassandra" = "Apache Cassandra" -"apache-drill" = "Apache Drill" -"apache-druid" = "Apache Druid" -"apache-flink" = "Apache Flink" -"apache-hdfs" = "Apache HDFS" -"apache-hive" = "Apache Hive" -"apache-impala" = "Apache Impala" -"apache-kafka" = "Apache Kafka" -"apache-kylin" = "Apache Kylin" -"apache-livy" = "Apache Livy" -"apache-pig" = "Apache Pig" -"apache-pinot" = "Apache Pinot" -"apache-spark" = "Apache Spark" -"apache-tinkerpop" = "Apache TinkerPop" -"celery" = "Celery" -"cohere" = "Cohere" -"common-compat" = "Common Compat" -"common-io" = "Common IO" -"common-messaging" = "Common Messaging" -"common-sql" = "Common SQL" -"databricks" = "Databricks" -"datadog" = "Datadog" -"dbt-cloud" = "dbt Cloud" -"dingding" = "DingTalk" -"discord" = "Discord" -"docker" = "Docker" -"edge3" = "Edge3" -"exasol" = "Exasol" -"fab" = "FAB" -"facebook" = "Facebook" -"git" = "Git" -"github" = "GitHub" -"grpc" = "gRPC" -"hashicorp" = "HashiCorp" -"jenkins" = "Jenkins" -"mongo" = "MongoDB" -"neo4j" = "Neo4j" -"openai" = "OpenAI" -"oracle" = "Oracle" -"pagerduty" = "PagerDuty" -"papermill" = "Papermill" -"pgvector" = "pgvector" -"pinecone" = "Pinecone" -"qdrant" = "Qdrant" -"redis" = "Redis" -"salesforce" = "Salesforce" -"samba" = "Samba" -"segment" = "Segment" -"sendgrid" = "SendGrid" -"singularity" = "Singularity" -"slack" = "Slack" -"snowflake" = "Snowflake" -"standard" = "Standard" -"tableau" = "Tableau" -"telegram" = "Telegram" -"teradata" = "Teradata" -"trino" = "Trino" -"vertica" = "Vertica" -"weaviate" = "Weaviate" -"yandex" = "Yandex" -"ydb" = "YDB" -"zendesk" = "Zendesk" +# Sub-project directory-name → vendor-preferred display casing for +# the CVE `product` field. Directory names are lowercase +# (`foo`, `acme-xyz`); CVE product names follow vendor-preferred +# casing (`Foo`, `Acme XYZ`). Extend this table when a new sub-project +# appears in a CVE; unknown names fall back to a title-cased +# dash-split of the directory name, which is correct for most +# single-word sub-projects but may need an entry here for acronyms. +[packages.project_display_map] +"foo" = "Foo" +"bar" = "Bar" +"kerfluffle" = "Kerfluffle" +"pop-corn" = "Pop Corn" +"xyz" = "XYZ" +"acme-xyz" = "Acme XYZ" diff --git a/tools/vulnogram/generate-cve-json/tests/test_generate_cve_json.py b/tools/vulnogram/generate-cve-json/tests/test_generate_cve_json.py index 5decd3cf..55809ab1 100644 --- a/tools/vulnogram/generate-cve-json/tests/test_generate_cve_json.py +++ b/tools/vulnogram/generate-cve-json/tests/test_generate_cve_json.py @@ -53,8 +53,8 @@ DEFAULT_AFFECTED_ARGS: dict[str, Any] = { "vendor": "Apache Software Foundation", - "product": "Apache Airflow", - "package_name": "apache-airflow", + "product": "Apache Example", + "package_name": "apache-example", "collection_url": "https://pypi.python.org", "version_start": None, } @@ -242,7 +242,7 @@ class TestParseAffectedVersionsNextVersionPlaceholder: """The `< NEXT VERSION` sentinel: fix not yet released, upper bound unknown. Stripped before further parsing; resulting entry has no `lessThan`. - Used predominantly for providers trackers where the wave milestone + Used predominantly for projects trackers where the wave milestone is date-based and the package version that ships the fix is decided by the release manager during the wave. """ @@ -288,39 +288,30 @@ def test_real_version_replacing_placeholder_round_trips(self): class TestProductForPackage: def test_core_package_resolves_to_apache_airflow(self): - assert _product_for_package("apache-airflow") == "Apache Airflow" + assert _product_for_package("apache-example") == "Apache Example" def test_known_provider_uses_display_map_casing(self): + assert _product_for_package("apache-example-project-foo") == "Apache Example Project Foo" assert ( - _product_for_package("apache-airflow-providers-elasticsearch") - == "Apache Airflow Providers Elasticsearch" - ) - assert ( - _product_for_package("apache-airflow-providers-opensearch") - == "Apache Airflow Providers OpenSearch" - ) - assert ( - _product_for_package("apache-airflow-providers-cncf-kubernetes") - == "Apache Airflow Providers CNCF Kubernetes" + _product_for_package("apache-example-project-kerfluffle") == "Apache Example Project Kerfluffle" ) + assert _product_for_package("apache-example-project-acme-xyz") == "Apache Example Project Acme XYZ" # Confirms the user-cited mapping example from the Vulnogram form: - # apache-airflow-providers-snowflake → "Apache Airflow Providers Snowflake". - assert ( - _product_for_package("apache-airflow-providers-snowflake") == "Apache Airflow Providers Snowflake" - ) + # apache-example-project-bar → "Apache Example Project Bar". + assert _product_for_package("apache-example-project-bar") == "Apache Example Project Bar" def test_unknown_provider_falls_back_to_title_case(self): assert ( - _product_for_package("apache-airflow-providers-madeup-widget") - == "Apache Airflow Providers Madeup Widget" + _product_for_package("apache-example-project-madeup-widget") + == "Apache Example Project Madeup Widget" ) def test_overrides_win_over_display_map(self): assert ( _product_for_package( - "apache-airflow-providers-elasticsearch", + "apache-example-project-foo", product_overrides={ - "apache-airflow-providers-elasticsearch": "Custom ES Display", + "apache-example-project-foo": "Custom ES Display", }, ) == "Custom ES Display" @@ -329,12 +320,12 @@ def test_overrides_win_over_display_map(self): def test_overrides_win_over_title_case_fallback(self): assert ( _product_for_package( - "apache-airflow-providers-madeup-widget", + "apache-example-project-madeup-widget", product_overrides={ - "apache-airflow-providers-madeup-widget": "Apache Airflow Providers WIDGET", + "apache-example-project-madeup-widget": "Apache Example Project WIDGET", }, ) - == "Apache Airflow Providers WIDGET" + == "Apache Example Project WIDGET" ) @@ -347,29 +338,29 @@ class TestBuildAffectedSingleProduct: def test_empty_field_emits_one_placeholder_entry(self): entries = build_affected("", **DEFAULT_AFFECTED_ARGS) assert len(entries) == 1 - assert entries[0]["packageName"] == "apache-airflow" - assert entries[0]["product"] == "Apache Airflow" + assert entries[0]["packageName"] == "apache-example" + assert entries[0]["product"] == "Apache Example" def test_bare_version_range_uses_defaults(self): entries = build_affected("<3.2.2", **DEFAULT_AFFECTED_ARGS) assert len(entries) == 1 - assert entries[0]["packageName"] == "apache-airflow" - assert entries[0]["product"] == "Apache Airflow" + assert entries[0]["packageName"] == "apache-example" + assert entries[0]["product"] == "Apache Example" assert entries[0]["versions"][0]["lessThan"] == "3.2.2" def test_single_line_with_package_prefix_detects_provider(self): entries = build_affected( - "apache-airflow-providers-elasticsearch <=6.5.0", + "apache-example-project-foo <=6.5.0", **DEFAULT_AFFECTED_ARGS, ) assert len(entries) == 1 - assert entries[0]["packageName"] == "apache-airflow-providers-elasticsearch" - assert entries[0]["product"] == "Apache Airflow Providers Elasticsearch" + assert entries[0]["packageName"] == "apache-example-project-foo" + assert entries[0]["product"] == "Apache Example Project Foo" assert entries[0]["versions"][0]["lessThanOrEqual"] == "6.5.0" def test_single_line_with_core_package_prefix_detects_core(self): entries = build_affected( - "apache-airflow <3.2.2", + "apache-example <3.2.2", vendor="Apache Software Foundation", product="IGNORED", package_name="ignored", @@ -379,59 +370,59 @@ def test_single_line_with_core_package_prefix_detects_core(self): assert len(entries) == 1 # Package prefix wins over the default argument because the body # has said the package name explicitly. - assert entries[0]["packageName"] == "apache-airflow" - assert entries[0]["product"] == "Apache Airflow" + assert entries[0]["packageName"] == "apache-example" + assert entries[0]["product"] == "Apache Example" class TestBuildAffectedMultiProduct: def test_two_providers_produce_two_entries(self): entries = build_affected( - "apache-airflow-providers-elasticsearch <=6.5.0\napache-airflow-providers-opensearch <=1.9.0", + "apache-example-project-foo <=6.5.0\napache-example-project-kerfluffle <=1.9.0", **DEFAULT_AFFECTED_ARGS, ) assert len(entries) == 2 es, os_ = entries - assert es["packageName"] == "apache-airflow-providers-elasticsearch" - assert es["product"] == "Apache Airflow Providers Elasticsearch" + assert es["packageName"] == "apache-example-project-foo" + assert es["product"] == "Apache Example Project Foo" assert es["versions"][0]["lessThanOrEqual"] == "6.5.0" - assert os_["packageName"] == "apache-airflow-providers-opensearch" - assert os_["product"] == "Apache Airflow Providers OpenSearch" + assert os_["packageName"] == "apache-example-project-kerfluffle" + assert os_["product"] == "Apache Example Project Kerfluffle" assert os_["versions"][0]["lessThanOrEqual"] == "1.9.0" def test_bullet_prefixes_are_stripped(self): entries = build_affected( - "- apache-airflow-providers-elasticsearch <=6.5.0\n* apache-airflow-providers-opensearch <=1.9.0", + "- apache-example-project-foo <=6.5.0\n* apache-example-project-kerfluffle <=1.9.0", **DEFAULT_AFFECTED_ARGS, ) assert [e["packageName"] for e in entries] == [ - "apache-airflow-providers-elasticsearch", - "apache-airflow-providers-opensearch", + "apache-example-project-foo", + "apache-example-project-kerfluffle", ] def test_blank_lines_between_entries_are_ignored(self): entries = build_affected( - "apache-airflow-providers-elasticsearch <=6.5.0\n\napache-airflow-providers-opensearch <=1.9.0\n", + "apache-example-project-foo <=6.5.0\n\napache-example-project-kerfluffle <=1.9.0\n", **DEFAULT_AFFECTED_ARGS, ) assert len(entries) == 2 def test_mixed_known_and_unknown_provider(self): entries = build_affected( - "apache-airflow-providers-elasticsearch <=6.5.0\napache-airflow-providers-brand-new <=0.1.0", + "apache-example-project-foo <=6.5.0\napache-example-project-brand-new <=0.1.0", **DEFAULT_AFFECTED_ARGS, ) - assert entries[0]["product"] == "Apache Airflow Providers Elasticsearch" - assert entries[1]["product"] == "Apache Airflow Providers Brand New" + assert entries[0]["product"] == "Apache Example Project Foo" + assert entries[1]["product"] == "Apache Example Project Brand New" def test_product_overrides_applied_per_entry(self): entries = build_affected( - "apache-airflow-providers-brand-new <=0.1.0", + "apache-example-project-brand-new <=0.1.0", product_overrides={ - "apache-airflow-providers-brand-new": "Apache Airflow Providers BRAND", + "apache-example-project-brand-new": "Apache Example Project BRAND", }, **DEFAULT_AFFECTED_ARGS, ) - assert entries[0]["product"] == "Apache Airflow Providers BRAND" + assert entries[0]["product"] == "Apache Example Project BRAND" def test_line_without_prefix_falls_back_to_defaults(self): # A single line that is not a version range and doesn't carry a @@ -440,24 +431,24 @@ def test_line_without_prefix_falls_back_to_defaults(self): entries = build_affected( "all versions", vendor="Apache Software Foundation", - product="Apache Airflow Helm Chart", - package_name="apache-airflow-helm-chart", + product="Apache Example Helm Chart", + package_name="apache-example-helm-chart", collection_url="https://airflow.apache.org/", version_start=None, ) assert len(entries) == 1 - assert entries[0]["packageName"] == "apache-airflow-helm-chart" - assert entries[0]["product"] == "Apache Airflow Helm Chart" + assert entries[0]["packageName"] == "apache-example-helm-chart" + assert entries[0]["product"] == "Apache Example Helm Chart" def test_multi_line_is_deterministic(self): # Re-generating must produce byte-identical output, which is # what keeps the `--attach` idempotence guarantee intact. first = build_affected( - "apache-airflow-providers-elasticsearch <=6.5.0\napache-airflow-providers-opensearch <=1.9.0", + "apache-example-project-foo <=6.5.0\napache-example-project-kerfluffle <=1.9.0", **DEFAULT_AFFECTED_ARGS, ) second = build_affected( - "apache-airflow-providers-elasticsearch <=6.5.0\napache-airflow-providers-opensearch <=1.9.0", + "apache-example-project-foo <=6.5.0\napache-example-project-kerfluffle <=1.9.0", **DEFAULT_AFFECTED_ARGS, ) assert first == second @@ -466,38 +457,37 @@ def test_per_line_next_version_placeholder(self): # The providers-tracker pattern: one line per affected package, # all with `< NEXT VERSION` until the wave ships. entries = build_affected( - "apache-airflow-providers-elasticsearch < NEXT VERSION\n" - "apache-airflow-providers-opensearch < NEXT VERSION", + "apache-example-project-foo < NEXT VERSION\napache-example-project-kerfluffle < NEXT VERSION", **DEFAULT_AFFECTED_ARGS, ) assert len(entries) == 2 es, os_ = entries - assert es["packageName"] == "apache-airflow-providers-elasticsearch" + assert es["packageName"] == "apache-example-project-foo" assert es["versions"] == [ {"status": "affected", "version": "0", "versionType": "semver"}, ] - assert os_["packageName"] == "apache-airflow-providers-opensearch" + assert os_["packageName"] == "apache-example-project-kerfluffle" assert os_["versions"] == [ {"status": "affected", "version": "0", "versionType": "semver"}, ] def test_per_line_lower_bound_only(self): - # Lower-bound-only line (e.g. `apache-airflow-providers-smtp >=2.0.0`) + # Lower-bound-only line (e.g. `apache-example-project-xyz >=2.0.0`) # — useful when affected versions are known to start at X but the # fix-shipped version isn't known yet. entries = build_affected( - "apache-airflow-providers-smtp >=2.0.0", + "apache-example-project-xyz >=2.0.0", **DEFAULT_AFFECTED_ARGS, ) assert len(entries) == 1 - assert entries[0]["packageName"] == "apache-airflow-providers-smtp" + assert entries[0]["packageName"] == "apache-example-project-xyz" assert entries[0]["versions"] == [ {"status": "affected", "version": "2.0.0", "versionType": "semver"}, ] def test_per_line_lower_bound_with_next_version_upper(self): entries = build_affected( - "apache-airflow-providers-smtp >= 2.0.0, < NEXT VERSION", + "apache-example-project-xyz >= 2.0.0, < NEXT VERSION", **DEFAULT_AFFECTED_ARGS, ) assert len(entries) == 1 @@ -568,7 +558,7 @@ def test_airflow_s_and_cveprocess_urls_are_filtered_out(self): mailing_list_field="", pr_field="", extra_urls=[ - "https://github.com/airflow-s/airflow-s/issues/256", + "https://github.com/apache-example-s/apache-example-s/issues/256", "https://cveprocess.apache.org/cve5/CVE-2026-40948", "https://github.com/apache/airflow/pull/64114", ], @@ -584,7 +574,7 @@ def test_airflow_s_and_cveprocess_urls_are_filtered_out(self): class TestResolveTitle: def test_strips_apache_airflow_prefix_from_issue_title(self): - assert resolve_title("Apache Airflow: DAG auth bypass", "", None) == "DAG auth bypass" + assert resolve_title("Apache Example: DAG auth bypass", "", None) == "DAG auth bypass" def test_override_wins(self): assert resolve_title("from-issue", "summary", "my override") == "my override" @@ -606,8 +596,8 @@ def _ready_cna() -> dict: "affected": build_affected( ">=3.0.0, <3.2.0", vendor="Apache Software Foundation", - product="Apache Airflow", - package_name="apache-airflow", + product="Apache Example", + package_name="apache-example", collection_url="https://pypi.python.org", version_start=None, ), @@ -711,8 +701,8 @@ def test_vendor_advisory_with_incomplete_fields_still_draft(self): def test_envelope_carries_cna_private_block(self): record = wrap_cve_record(_ready_cna(), cve_id="CVE-2026-00001", org_id="org") - assert record["CNA_private"]["owner"] == "airflow" - assert record["CNA_private"]["userslist"] == "users@airflow.apache.org" + assert record["CNA_private"]["owner"] == "example" + assert record["CNA_private"]["userslist"] == "users@example.apache.org" # --------------------------------------------------------------------------- @@ -747,27 +737,27 @@ def test_incomplete_is_draft(self): class TestComputePackageUrl: def test_pypi_python_org_returns_canonical_pypi_project_url(self): assert ( - compute_package_url("https://pypi.python.org", "apache-airflow") - == "https://pypi.org/project/apache-airflow/" + compute_package_url("https://pypi.python.org", "apache-example") + == "https://pypi.org/project/apache-example/" ) def test_pypi_org_alias_also_supported(self): assert ( - compute_package_url("https://pypi.org", "apache-airflow") - == "https://pypi.org/project/apache-airflow/" + compute_package_url("https://pypi.org", "apache-example") + == "https://pypi.org/project/apache-example/" ) def test_trailing_slash_is_tolerated(self): assert ( - compute_package_url("https://pypi.python.org/", "apache-airflow-providers-elasticsearch") - == "https://pypi.org/project/apache-airflow-providers-elasticsearch/" + compute_package_url("https://pypi.python.org/", "apache-example-project-foo") + == "https://pypi.org/project/apache-example-project-foo/" ) def test_unknown_collection_url_returns_none(self): - assert compute_package_url("https://airflow.apache.org/", "apache-airflow-helm-chart") is None + assert compute_package_url("https://airflow.apache.org/", "apache-example-helm-chart") is None def test_empty_inputs_return_none(self): - assert compute_package_url("", "apache-airflow") is None + assert compute_package_url("", "apache-example") is None assert compute_package_url("https://pypi.python.org", "") is None @@ -835,7 +825,7 @@ def test_metric_table_includes_title_state_and_size(self): assert "| CVE ID | `CVE-2026-00001` |" in body assert "| Title | Example vulnerability |" in body assert "| Vulnogram state | `REVIEW` |" in body - assert "| Affected packages | `apache-airflow` |" in body + assert "| Affected packages | `apache-example` |" in body assert "| Size | 8 bytes |" in body def test_no_envelope_renders_state_placeholder(self): @@ -857,17 +847,17 @@ def test_per_package_table_includes_pypi_url_and_versions(self): assert "**Packages this JSON covers:**" in body assert "| # | Package | Product | Versions | PyPI URL |" in body assert ( - "| 1 | `apache-airflow` | Apache Airflow | `>= 3.0.0, < 3.2.0` | |" + "| 1 | `apache-example` | Apache Example | `>= 3.0.0, < 3.2.0` | |" in body ) def test_per_package_table_renders_one_row_per_affected_entry(self): cna = _ready_cna_for_attachment() cna["affected"] = build_affected( - "apache-airflow-providers-elasticsearch <=6.5.0\napache-airflow-providers-opensearch <=1.9.0", + "apache-example-project-foo <=6.5.0\napache-example-project-kerfluffle <=1.9.0", vendor="Apache Software Foundation", - product="Apache Airflow", - package_name="apache-airflow", + product="Apache Example", + package_name="apache-example", collection_url="https://pypi.python.org", version_start=None, ) @@ -878,16 +868,15 @@ def test_per_package_table_renders_one_row_per_affected_entry(self): cna_private_state="REVIEW", ) assert ( - "| 1 | `apache-airflow-providers-elasticsearch` | Apache Airflow Providers Elasticsearch " - "| `<= 6.5.0` | |" + "| 1 | `apache-example-project-foo` | Apache Example Project Foo " + "| `<= 6.5.0` | |" ) in body assert ( - "| 2 | `apache-airflow-providers-opensearch` | Apache Airflow Providers OpenSearch " - "| `<= 1.9.0` | |" + "| 2 | `apache-example-project-kerfluffle` | Apache Example Project Kerfluffle " + "| `<= 1.9.0` | |" ) in body assert ( - "| Affected packages | `apache-airflow-providers-elasticsearch`, " - "`apache-airflow-providers-opensearch` |" + "| Affected packages | `apache-example-project-foo`, `apache-example-project-kerfluffle` |" ) in body def test_unknown_collection_url_renders_dash_in_url_column(self): @@ -895,8 +884,8 @@ def test_unknown_collection_url_renders_dash_in_url_column(self): cna["affected"] = [ { "vendor": "Apache Software Foundation", - "product": "Apache Airflow Helm Chart", - "packageName": "apache-airflow-helm-chart", + "product": "Apache Example Helm Chart", + "packageName": "apache-example-helm-chart", "collectionURL": "https://airflow.apache.org/", "versions": [ {"version": "0", "lessThan": "1.18.0", "status": "affected", "versionType": "semver"}, @@ -910,7 +899,7 @@ def test_unknown_collection_url_renders_dash_in_url_column(self): cna=cna, cna_private_state="REVIEW", ) - assert "| 1 | `apache-airflow-helm-chart` | Apache Airflow Helm Chart | `< 1.18.0` | — |" in body + assert "| 1 | `apache-example-helm-chart` | Apache Example Helm Chart | `< 1.18.0` | — |" in body def test_pipe_in_title_is_escaped(self): cna = _ready_cna_for_attachment() diff --git a/uv.lock b/uv.lock index 4b0eda0a..f5a819da 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ exclude-newer-span = "P7D" uv = { timestamp = "0001-01-01T00:00:00Z", span = "P1D" } [[package]] -name = "apache-airflow-steward" +name = "apache-steward" version = "0.0.0" source = { virtual = "." }