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. View guided diff Turn on auto-fix ## 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[@]}"