From 08bf62ff4d940a99c0274e5286679ea3e709986f Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 23 Mar 2026 15:36:45 -0500 Subject: [PATCH] feat(vulnerability-remediation): support upgrading transitive dependencies without adding as top-level Port changes from OpenHands/vulnerability-fixer#48 When a CVE fix requires upgrading a transitive dependency (not directly declared in manifest files), the remediation agent now attempts to upgrade it without adding it as a top-level dependency in the manifest file. Changes: - Added Step 2: Determine if direct or transitive dependency - Added Step 2a: Upgrading TRANSITIVE Dependencies with package manager-specific commands - Updated UV lockfile section with transitive upgrade guidance - Updated README with Direct vs Transitive Dependencies documentation Co-authored-by: openhands --- plugins/vulnerability-remediation/README.md | 41 ++++++-- .../scripts/scan_and_remediate.py | 96 ++++++++++++++++--- 2 files changed, 117 insertions(+), 20 deletions(-) diff --git a/plugins/vulnerability-remediation/README.md b/plugins/vulnerability-remediation/README.md index f93efed..29d6b01 100644 --- a/plugins/vulnerability-remediation/README.md +++ b/plugins/vulnerability-remediation/README.md @@ -146,16 +146,45 @@ You can also trigger scans manually: When remediating a vulnerability, the agent: 1. **Analyzes** the vulnerability details (CVE ID, affected package, versions) -2. **Locates** the dependency file (package.json, requirements.txt, pom.xml, etc.) -3. **Updates** the package to the fixed version -4. **Verifies** the change doesn't break the build -5. **Creates a branch** named `fix/` -6. **Commits** changes with a descriptive message -7. **Creates a PR** with: +2. **Determines** if the package is a direct or transitive dependency +3. **For transitive dependencies**: Attempts lockfile-only upgrade without adding to manifest +4. **For direct dependencies**: Updates the package version in manifest files +5. **Regenerates** all corresponding lockfiles +6. **Verifies** the change doesn't break the build +7. **Creates a branch** named `fix/` +8. **Commits** changes with a descriptive message +9. **Creates a PR** with: - Vulnerability details - What was changed - Links to CVE references +### Direct vs Transitive Dependencies + +When fixing a CVE, the agent first determines whether the vulnerable package is a **direct dependency** (explicitly declared in manifest files) or a **transitive dependency** (pulled in by another package). + +**For transitive dependencies**, the agent tries to upgrade them **without adding them as top-level dependencies** in manifest files. This keeps dependencies minimal and avoids unnecessary changes. + +**For direct dependencies**, the agent updates the version in the manifest file(s) and regenerates lockfiles. + +### Transitive Dependency Upgrades + +Most modern package managers support upgrading specific transitive dependencies in the lockfile without adding them to manifest files: + +**General approach:** +1. First, check if the package appears in any manifest files (pyproject.toml, package.json, requirements.txt, etc.) +2. If NOT found → it's a transitive dependency → attempt lockfile-only upgrade +3. If lockfile upgrade succeeds → commit without modifying manifest files +4. If lockfile upgrade fails → fall back to adding as direct dependency + +**Package manager commands for transitive upgrades:** +- **uv**: `uv lock --upgrade-package ` +- **Poetry**: `poetry update ` (updates the package in poetry.lock) +- **npm**: `npm update ` or use `overrides` in package.json +- **yarn**: `yarn upgrade ` or use `resolutions` in package.json +- **pnpm**: `pnpm update ` or use `overrides` in package.json +- **Cargo**: `cargo update -p ` (updates Cargo.lock) +- **Go**: `go get @v` followed by `go mod tidy` + ## Supported Package Ecosystems Trivy scans and the remediation agent support: diff --git a/plugins/vulnerability-remediation/scripts/scan_and_remediate.py b/plugins/vulnerability-remediation/scripts/scan_and_remediate.py index 99b45ff..3c0f4fd 100644 --- a/plugins/vulnerability-remediation/scripts/scan_and_remediate.py +++ b/plugins/vulnerability-remediation/scripts/scan_and_remediate.py @@ -177,19 +177,85 @@ def create_remediation_prompt(vuln: Vulnerability, repo_name: str) -> str: 1. **Analyze** the vulnerability and understand what needs to be fixed -2. **Find ALL dependency files across the repository** - **CRITICAL**: Search the repository for ALL dependency files that contain the vulnerable package. Do NOT assume dependencies only exist in the root directory. +2. **Determine if the package is a DIRECT or TRANSITIVE dependency** + **CRITICAL**: Before modifying any files, determine whether `{vuln.package_name}` is a direct dependency (explicitly listed in manifest files) or a transitive dependency (pulled in by another package). ```bash - # Find ALL dependency files containing the package (excludes dependency/build directories) - find . \( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git -o -name __pycache__ -o -name dist -o -name build \) -prune -o -name "pyproject.toml" -exec grep -l "{vuln.package_name}" {} + 2>/dev/null - find . \( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git \) -prune -o -name "requirements*.txt" -exec grep -l "{vuln.package_name}" {} + 2>/dev/null - find . \( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git \) -prune -o -name "package.json" -exec grep -l "{vuln.package_name}" {} + 2>/dev/null + # Check if the package is listed as a DIRECT dependency in manifest files + echo "=== Checking for direct dependency declarations ===" + find . \\( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git -o -name __pycache__ -o -name dist -o -name build \\) -prune -o -name "pyproject.toml" -print | xargs grep -l "{vuln.package_name}" 2>/dev/null + find . \\( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git \\) -prune -o -name "requirements*.txt" -print | xargs grep -l "{vuln.package_name}" 2>/dev/null + find . \\( -name node_modules -o -name .venv -o -name venv -o -name vendor -o -name .git \\) -prune -o -name "package.json" -print | xargs grep -l "{vuln.package_name}" 2>/dev/null ``` - Update {vuln.package_name} from {vuln.installed_version} to {vuln.fixed_version} in **EVERY** file found. + **If {vuln.package_name} is found in manifest files**: It's a DIRECT dependency - proceed to update those files (Step 3). -3. **CRITICAL: Sync/regenerate ALL lockfiles in the repository** + **If {vuln.package_name} is NOT found in any manifest files**: It's a TRANSITIVE dependency - try to upgrade it via lockfile tools FIRST (Step 2a) before falling back to adding as a direct dependency. + +### 2a. Upgrading TRANSITIVE Dependencies (if applicable) +If the package is a transitive dependency, try upgrading it without adding to manifest files. This is the preferred approach as it keeps dependencies minimal. + +**General Principle**: Most modern package managers support upgrading specific transitive dependencies in the lockfile without adding them as top-level dependencies: + +For EACH lockfile found, attempt the transitive upgrade: + +- **uv (uv.lock)**: + ```bash + # Upgrade transitive dependency without modifying pyproject.toml + uv lock --upgrade-package {vuln.package_name} + + # If the upgrade fails due to version constraints from another package, + # you may need to upgrade that parent package too: + uv lock --upgrade-package {vuln.package_name} --upgrade-package + ``` + +- **Poetry (poetry.lock)**: + ```bash + # Try updating just the specific package in the lockfile + poetry update {vuln.package_name} + ``` + +- **npm (package-lock.json)**: + ```bash + # npm can update nested dependencies via overrides in package.json or: + npm update {vuln.package_name} + ``` + +- **yarn (yarn.lock)**: + ```bash + # yarn supports resolutions in package.json to override transitive deps + yarn upgrade {vuln.package_name} + ``` + +- **pnpm (pnpm-lock.yaml)**: + ```bash + # pnpm supports overrides in package.json + pnpm update {vuln.package_name} + ``` + +- **Cargo (Cargo.lock)**: + ```bash + # Cargo can update transitive deps + cargo update -p {vuln.package_name} + ``` + +- **Go (go.sum)**: + ```bash + # Go modules can update transitive deps + go get {vuln.package_name}@v{vuln.fixed_version} + go mod tidy + ``` + +**After attempting transitive upgrade**: Verify the upgrade succeeded by checking the lockfile for the new version. If successful, skip Step 3 and proceed to Step 4 (lockfile regeneration). + +**If transitive upgrade fails** (e.g., version constraints prevent it): Fall back to adding as a direct dependency in Step 3. + +3. **Update DIRECT dependencies in manifest files** + **Only if the package is a direct dependency OR transitive upgrade failed:** + + Update {vuln.package_name} from {vuln.installed_version} to {vuln.fixed_version} in **EVERY** manifest file where it appears. + +4. **CRITICAL: Sync/regenerate ALL lockfiles in the repository** After updating the version in ALL manifest files, you MUST regenerate ALL corresponding lockfiles. Do NOT manually edit lockfiles. **CRITICAL**: First, find ALL lockfiles (excluding dependency/build directories): @@ -219,7 +285,9 @@ def create_remediation_prompt(vuln: Vulnerability, repo_name: str) -> str: 3. If a version is found, install it: `pipx install uv==$UV_VERSION --force` 4. Verify installation: `uv --version | grep "$UV_VERSION"` (proceed only if successful) 5. If version extraction fails or returns empty, proceed with the currently installed version and note this in your output - 6. Run: `uv lock --upgrade-package {vuln.package_name}` or `uv sync` + 6. For transitive deps: `uv lock --upgrade-package {vuln.package_name}` + If constraints prevent upgrade: `uv lock --upgrade-package {vuln.package_name} --upgrade-package ` + For direct deps after manifest update: `uv sync` - **npm (package.json + package-lock.json)**: `cd` to directory, run `npm install` or `npm update {vuln.package_name}` - **yarn (package.json + yarn.lock)**: `cd` to directory, run `yarn install` or `yarn upgrade {vuln.package_name}` @@ -230,11 +298,11 @@ def create_remediation_prompt(vuln: Vulnerability, repo_name: str) -> str: - **Maven (pom.xml)**: Update the version directly in pom.xml - **Gradle**: Update the version in build.gradle/build.gradle.kts -4. **Verify** the change doesn't break the build (run any available build/test commands) -5. **Create a branch** named `fix/{vuln.vuln_id.lower()}` -6. **Commit** your changes with a clear message explaining the security fix -7. **Push** the branch to origin -8. **Create a Pull Request** using the GitHub CLI: +5. **Verify** the change doesn't break the build (run any available build/test commands) +6. **Create a branch** named `fix/{vuln.vuln_id.lower()}` +7. **Commit** your changes with a clear message explaining the security fix +8. **Push** the branch to origin +9. **Create a Pull Request** using the GitHub CLI: ```bash gh pr create --title "fix: {vuln.vuln_id} - Update {vuln.package_name} to {vuln.fixed_version}" \\ --body "## Security Fix