potiuk opened a new pull request, #363: URL: https://github.com/apache/airflow-steward/pull/363
## Summary The default Vulnogram API push (`vulnogram-api-record-update`) is a full record replacement: whatever JSON the script sends becomes the record. That model has bitten the Apache Airflow security team in three concrete ways during 2026, all surfaced on `CVE-2026-41016`'s reviewer-comment thread on 2026-05-28: | Failure | Trigger | |---|---| | `PUBLIC` → `REVIEW` state regression | regenerated re-push from a sibling tracker walked the published state backwards | | `apache-airflow-providers-smtp` → `apache-airflow` product change | regenerator's scope-label resolution changed the affected package post-publication | | Lost `references[]` advisory URL | regenerator emits references from the tracker body only; the hand-added archive URL got blasted | ## Changes Three guard checks fire before the POST. All three are opt-out via explicit flags so deliberate changes still work: | Flag | Behaviour | |---|---| | (default) | refuse `PUBLIC` → `REVIEW / DRAFT / READY` | | `--allow-state-downgrade` | allow the transition | | (default) | merge `references[]` by URL — preserve current entries not in new emission | | `--replace-references` | replace `references[]` wholesale | | (default) | refuse `affected[].product` / `packageName` changes | | `--allow-product-change` | allow the change | | `--full-replace` | umbrella for all three overrides | The guards live in a new `vulnogram_api.merge_mode` module so the contract is testable in isolation. The CLI fetches the current record via `get_record` before the push (no-op when the record doesn't exist yet — first push for a CVE ID), passes both docs through `apply_merge_mode_guards`, and pushes the merged document. New exit code **3** specifically reports a merge-mode refusal so a scripted caller can distinguish a guard refusal from a transport / validation failure. ## Test plan - [x] 84 tests pass (`uv run pytest`). - [x] 12 new tests in `test_merge_mode.py` covering each guard's refusal + override paths, references-by-URL merge semantics, no-current-record (first push) no-op, deep-copy isolation of the input doc. - [x] 7 new tests in `test_record_update.py` exercising the CLI end-to-end against mocked `get_record` / `update_record`. - [x] `mypy` clean, `ruff format` / `ruff check` clean. - [x] Existing CLI tests still pass with the new fetch-current-before-push behaviour (mocked `_fetch_current_or_none` returns `None` to keep them on the "first push, no merge" path). ## Related - Caps the four-PR diagnostic series from Arnout's 2026-05-28 review pass: #360 (gate REVIEW on RC vote), #361 (sync-skill `[VOTE]` detection), #362 (CWE wrapper + version sanitization), this PR (record-update merge-mode). - Closes the regen-overwrites-manual-edits class identified in CVE-2026-41016's revert: with all three guards on, the regression that prompted the revert would have been refused at push time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
