From 4f69237d856dea09bc01fedaef53b628cda7ec70 Mon Sep 17 00:00:00 2001
From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com>
Date: Wed, 21 Oct 2026 02:01:49 +0000
Subject: [PATCH] ci(cve-alerts): let the fix bot edit compiled requirements
files (#16362)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
_Requested by **Nikolas Garza** ยท [Slack
thread](https://onyx-company.slack.com/archives/C0C5SMAM4TT/p1792538621965719?thread_ts=1792538621.965719&cid=C0C5SMAM4TT)_
Before: The CVE Alerts fix bot could edit only `package.json` and
`pyproject.toml` (plus regenerated lockfiles). It could not fix a pin in
`initial-requirements.txt`, so the audit kept failing.
After: The bot can also edit compiled requirements files
(`*requirements*.txt`, today only `initial-requirements.txt`). It still
rejects source code, workflows, and all other files.
This lets the bot fix a pin in any manifest file that the audit can fail
on and that the workflow can safely validate.
How: PR #16337 missed a certifi pin in `initial-requirements.txt`, and
#16361 fixed it by hand. The change adds `**/*requirements*.txt` to the
editable file list and to the patch allowlist in `cve-alerts.yml`.
`cve-check-manifests.ts` now checks `.txt` edits: only the package's
`==` line may change, and only to the fixed version. `cve-fix.md` tells
the bot to edit that line and keep the header and `# via` comments.
`backend/requirements/*.txt` stay workflow-regenerated and off-limits to
the bot. Go and Rust alerts stay out of scope, because the bot has no
regeneration step for them.
Linear: none
---
_Generated by [Claude
Code](https://claude.ai/code/session_01E2K27DFf4J3iAGXYpe4xtR)_
---
## Summary by cubic
Lets the CVE Alerts fix bot edit compiled requirements files so it can
fix a pin in `initial-requirements.txt` instead of leaving the audit
failing.
- Requirements files are validated line by line: only the package's
plain `==` pin may change to the fixed version, byte-identical
elsewhere.
- `backend/requirements/*.txt` stay workflow-regenerated and off-limits;
source code, workflows, and other files are still rejected.
Written for commit 17a29722b7a5c0c5930d3264151c30aaa1346f0a.
Summary will update on new commits.
## Summary by CodeRabbit
- **Bug Fixes**
- CVE checks now include supported Python `requirements*.txt` files
alongside existing dependency manifests.
- Dependency updates are validated line by line to require exact fixed
versions while preserving file structure and metadata.
- Unsupported requirement syntax and unsafe version changes are rejected
during validation.
- **Documentation**
- Dependency-fix guidance now covers compiled requirements files and
preserves generated metadata during updates.
---------
Co-authored-by: Claude
---
.github/prompts/cve-fix.md | 17 +++++++--
.github/scripts/cve-check-manifests.ts | 53 +++++++++++++++++++++++++-
.github/workflows/cve-alerts.yml | 8 ++--
3 files changed, 69 insertions(+), 9 deletions(-)
diff --git a/.github/prompts/cve-fix.md b/.github/prompts/cve-fix.md
index 03696d1dac..93f6d97b5c 100644
--- a/.github/prompts/cve-fix.md
+++ b/.github/prompts/cve-fix.md
@@ -34,10 +34,19 @@ their own update jobs.
only as a transitive dependency, add it to that project's `overrides`.
- PyPI: update the pin in `pyproject.toml` if there is one. A package with no
pin needs no edit, since the workflow locks the version you report.
-- Edit only this package's dependency entries in existing `package.json` and
- `pyproject.toml` files, and set each one to exactly the version you report.
- Do not edit lockfiles. The workflow regenerates them and rejects any other
- change. You have no shell, so re-read each edited file to confirm.
+- A compiled requirements file is the output of `uv pip compile`. When it has a
+ `==` line, change only the version after `==` on that line, even
+ for a transitive package. Keep the header comments and the `# via` lines.
+ Leave an entry alone when its line has a marker (`;`), an extra, a comment, a
+ `\` continuation, an `@` URL, or an option. The workflow rejects any other
+ change to the file. The
+ workflow does not regenerate these files. `backend/requirements/*.txt` are
+ exports that the workflow regenerates, so never edit them.
+- Edit only this package's dependency entries in existing `package.json`,
+ `pyproject.toml`, and compiled requirements files, and set each one to exactly
+ the version you report. Do not edit lockfiles. The workflow regenerates them
+ and rejects any other change. You have no shell, so re-read each edited file
+ to confirm.
## 2. Judge whether Onyx is affected
diff --git a/.github/scripts/cve-check-manifests.ts b/.github/scripts/cve-check-manifests.ts
index 09fbc4bc9f..7c3974c82b 100644
--- a/.github/scripts/cve-check-manifests.ts
+++ b/.github/scripts/cve-check-manifests.ts
@@ -1,6 +1,9 @@
// Usage: bun cve-check-manifests.ts ...
// Fails unless a manifest's only changes from HEAD pin the package's dependency
// entries to the fixed version, since CVE Alerts runs it on Claude's edits.
+// Manifests are package.json, pyproject.toml, and compiled requirements .txt files.
+// A requirements file may change only the version after "==" on the package's
+// plain pin line.
import { $ } from "bun";
type Table = Record;
@@ -21,6 +24,13 @@ const normalizePython = (name: string) =>
const requirementName = (requirement: string) =>
/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(requirement)?.[1] ?? "";
+const isRequirementsFile = (file: string) => file.endsWith(".txt");
+
+// A requirements line that starts the package's entry. Indented lines (the
+// "# via" comments, hashes) and options never match.
+const isRequirementLine = (line: string, pkg: string) =>
+ /^[A-Za-z0-9]/.test(line) && normalizePython(requirementName(line)) === normalizePython(pkg);
+
const isTable = (v: unknown): v is Table =>
v !== null && typeof v === "object" && !Array.isArray(v);
@@ -134,7 +144,41 @@ function badSpecs(file: string, beforeText: string, afterText: string, pkg: stri
return now.filter((r) => r.includes("@") || !pinned.test(r));
}
-function stripped(file: string, text: string, pkg: string): Table {
+// A compiled requirements file gets a line-by-line check, since a marker, a
+// continuation, a comment, or an option on a changed line can carry content that
+// a strip-and-compare would hide. Only a plain "name==version" line of the
+// package may change, and only its version.
+const PLAIN_PIN = /^([A-Za-z0-9][A-Za-z0-9._-]*==)([A-Za-z0-9._+!*-]+)(.*)$/s;
+const UNSAFE_PIN = /[\\#;@]|--/;
+
+function requirementsProblems(beforeText: string, afterText: string, pkg: string, version: string): string[] {
+ const was = beforeText.split("\n");
+ const now = afterText.split("\n");
+ if (was.length !== now.length) {
+ return [`line count changed from ${was.length} to ${now.length}`];
+ }
+ const problems: string[] = [];
+ now.forEach((line, i) => {
+ const old = was[i]!;
+ if (line === old) {
+ return;
+ }
+ const pin = PLAIN_PIN.exec(old);
+ const allowed =
+ version !== "" &&
+ isRequirementLine(old, pkg) &&
+ pin !== null &&
+ !UNSAFE_PIN.test(old) &&
+ !UNSAFE_PIN.test(line) &&
+ line === `${pin[1]}${version}${pin[3]}`;
+ if (!allowed) {
+ problems.push(`line ${i + 1} is not a plain version change of ${pkg}`);
+ }
+ });
+ return problems;
+}
+
+function stripped(file: string, text: string, pkg: string): unknown {
return file.endsWith(".toml")
? stripPython(Bun.TOML.parse(text) as Table, pkg)
: stripNpm(JSON.parse(text) as Table, pkg);
@@ -150,6 +194,13 @@ let failed = false;
for (const file of files) {
const beforeText = await $`git show HEAD:${file}`.text();
const text = await Bun.file(file).text();
+ if (isRequirementsFile(file)) {
+ for (const problem of requirementsProblems(beforeText, text, pkg, version)) {
+ console.log(`::error::${file} changes more than the ${pkg} version: ${problem}`);
+ failed = true;
+ }
+ continue;
+ }
if (!Bun.deepEquals(stripped(file, beforeText, pkg), stripped(file, text, pkg))) {
console.log(`::error::${file} changes more than ${pkg}`);
failed = true;
diff --git a/.github/workflows/cve-alerts.yml b/.github/workflows/cve-alerts.yml
index fe96630d8a..1f525c90da 100644
--- a/.github/workflows/cve-alerts.yml
+++ b/.github/workflows/cve-alerts.yml
@@ -510,7 +510,7 @@ jobs:
*) echo "::error::OSV returned ${code} for ${id}"; exit 1 ;;
esac
done
- git ls-files -- ':(glob)**/package.json' ':(glob)**/pyproject.toml' > "${RUNNER_TEMP}/manifests.txt"
+ git ls-files -- ':(glob)**/package.json' ':(glob)**/pyproject.toml' ':(glob)**/*requirements*.txt' ':!backend/requirements/*.txt' > "${RUNNER_TEMP}/manifests.txt"
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # ratchet:oven-sh/setup-bun@v2
with:
@@ -757,8 +757,8 @@ jobs:
exit 1
fi
files="$(git apply --numstat "${patch}" | cut -f3)"
- # Only manifests, lockfiles, and the exported requirements may change.
- if unexpected="$(grep -vE '(^|/)(package\.json|bun\.lock|pyproject\.toml|uv\.lock)$|^backend/requirements/[^/]+\.txt$' <<< "${files}")"; then
+ # Only manifests, lockfiles, and requirements files may change.
+ if unexpected="$(grep -vE '(^|/)(package\.json|bun\.lock|pyproject\.toml|uv\.lock)$|^backend/requirements/[^/]+\.txt$|(^|/)[^/]*requirements[^/]*\.txt$' <<< "${files}")"; then
echo "::error::Unexpected changes: ${unexpected}"
exit 1
fi
@@ -766,7 +766,7 @@ jobs:
manifests=()
while IFS= read -r f; do
manifests+=("${f}")
- done < <(grep -E '(^|/)(package\.json|pyproject\.toml)$' <<< "${files}" || true)
+ done < <(grep -E '(^|/)(package\.json|pyproject\.toml|[^/]*requirements[^/]*\.txt)$' <<< "${files}" || true)
if [ "${#manifests[@]}" -gt 0 ]; then
version="$(jq -r '.fixed_version // ""' "${RUNNER_TEMP}/triage/result.json")"
bun "${GITHUB_WORKSPACE}/.github/scripts/cve-check-manifests.ts" "${PACKAGE}" "${version}" "${manifests[@]}"