ci(cve-alerts): let the fix bot edit compiled requirements files (#16362)

<!-- claude-slack-attribution -->
_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)_

<!-- This is an auto-generated description by cubic. -->
---
## 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.

<sup>Written for commit 17a29722b7.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/onyx-dot-app/onyx/pull/16362?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="View guided diff"
src="https://www.cubic.dev/buttons/review-in-cubic-light.svg"></picture></a>
<a
href="https://www.cubic.dev/action/auto-fix/pr/onyx-dot-app/onyx/16362?returnTo=https%3A%2F%2Fgithub.com%2Fonyx-dot-app%2Fonyx%2Fpull%2F16362&source=description"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"><img
alt="Turn on auto-fix"
src="https://www.cubic.dev/buttons/turn-on-auto-fix-light.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
claude[bot]andClaude authored and GitHub committed 2026-10-21 02:01:49 +00:00
1 parent ae9b863612
commit 4f69237d85
3 files changed
+69 -9

No files matched your search

+52 -1
View File
@@ -1,6 +1,9 @@
// Usage: bun cve-check-manifests.ts <package> <fixed version> <file>...
// 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<string, unknown>;
@@ -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;