diff --git a/ymir/agents/prompts/backport/instructions.j2 b/ymir/agents/prompts/backport/instructions.j2 index 1a639d7b1..6ada9f3f0 100644 --- a/ymir/agents/prompts/backport/instructions.j2 +++ b/ymir/agents/prompts/backport/instructions.j2 @@ -76,9 +76,11 @@ a backport that won't actually fix the shipped RPM. PatchN: --.patch where is the upstream version from the spec file's Version field. -1. Knowing Jira issue , CVE ID or both, use the `git_log_search` tool to check - in the dist-git repository whether the issue/CVE has already been resolved. If it has, +1. Knowing Jira issue , CVE ID(s) or both, use the `git_log_search` tool to check + in the dist-git repository whether each issue/CVE has already been resolved. If it has, end the process with `success=True` and `status="Backport already applied"`. + Note: may contain multiple CVE IDs when the Jira issue covers multiple CVEs. + The `git_log_search` tool handles multiple CVE IDs automatically. 2. Use the `git_prepare_package_sources` tool to prepare package sources in directory for application of the upstream patch. diff --git a/ymir/agents/prompts/backport/instructions_zstream.j2 b/ymir/agents/prompts/backport/instructions_zstream.j2 index 338e92a2f..f5f72648c 100644 --- a/ymir/agents/prompts/backport/instructions_zstream.j2 +++ b/ymir/agents/prompts/backport/instructions_zstream.j2 @@ -64,9 +64,11 @@ a backport that won't actually fix the shipped RPM. Do NOT add comments above the Patch tag for z-stream branches unless existing patches in the spec already use comments (see Priority 2). -1. Knowing Jira issue , CVE ID or both, use the `git_log_search` tool to check - in the dist-git repository whether the issue/CVE has already been resolved. If it has, +1. Knowing Jira issue , CVE ID(s) or both, use the `git_log_search` tool to check + in the dist-git repository whether each issue/CVE has already been resolved. If it has, end the process with `success=True` and `status="Backport already applied"`. + Note: may contain multiple CVE IDs when the Jira issue covers several CVEs. + The `git_log_search` tool handles multiple CVE IDs automatically. 2. Use the `git_prepare_package_sources` tool to prepare package sources in directory for application of the upstream patch. diff --git a/ymir/agents/prompts/triage/output_format.j2 b/ymir/agents/prompts/triage/output_format.j2 index 34aa0e6c6..36771c49b 100644 --- a/ymir/agents/prompts/triage/output_format.j2 +++ b/ymir/agents/prompts/triage/output_format.j2 @@ -21,6 +21,8 @@ Backport resolution: } } ``` +Note: When the Jira issue covers multiple CVEs, include ALL CVE IDs in `cve_id`, e.g.: +`"cve_id": "CVE-1234-98765 CVE-1234-98766"` Rebase resolution: ```json @@ -36,6 +38,8 @@ Rebase resolution: } } ``` +Note: When the Jira issue covers multiple CVEs, include ALL CVE IDs in `cve_id`, e.g.: +`"cve_id": "CVE-1234-98765 CVE-1234-98766"` Rebuild resolution: ```json @@ -53,6 +57,8 @@ Rebuild resolution: } } ``` +Note: When the Jira issue covers multiple CVEs, include ALL CVE IDs in `cve_id`, e.g.: +`"cve_id": "CVE-1234-98765 CVE-1234-98766"` Postponed resolution (rebuild waiting for dependency): ```json @@ -70,6 +76,8 @@ Postponed resolution (rebuild waiting for dependency): } } ``` +Note: When the Jira issue covers multiple CVEs, include ALL CVE IDs in `cve_id`, e.g.: +`"cve_id": "CVE-1234-98765 CVE-1234-98766"` Not-affected resolution (CVE does not apply): ```json diff --git a/ymir/agents/prompts/triage/prompt.j2 b/ymir/agents/prompts/triage/prompt.j2 index 4077c52c0..1b7b71998 100644 --- a/ymir/agents/prompts/triage/prompt.j2 +++ b/ymir/agents/prompts/triage/prompt.j2 @@ -165,8 +165,10 @@ You must decide between one of the following actions. Follow these guidelines to * The code changes address the specific vulnerability type (buffer overflow, integer overflow, etc.) * The affected functions/files align with the CVE details - - **WARNING**: Patches from bundled CVE updates (e.g., Oracle CPU, bundled library updates) - may fix MULTIPLE CVEs - verify you have the correct patch for THIS specific CVE + - **Multiple CVEs in one issue**: When the Jira issue covers multiple CVEs, + verify you have the correct patches for ALL of them — not just one. + Include ALL CVE IDs in the `cve_id` output field so the downstream + backport agent knows the full scope and applies all related patches. - If you cannot confirm the patch matches the CVE, search for alternative patches or request clarification * Only proceed with URLs that contain valid patch content AND address the specific issue diff --git a/ymir/common/models.py b/ymir/common/models.py index e88099f36..f97960af8 100644 --- a/ymir/common/models.py +++ b/ymir/common/models.py @@ -141,7 +141,10 @@ class BackportInputSchema(BaseModel): package: str = Field(description="Package to update") dist_git_branch: str = Field(description="Git branch in dist-git to be updated") jira_issue: str = Field(description="Jira issue to reference as resolved") - cve_id: str | None = Field(default=None, description="CVE ID if the jira issue is a CVE") + cve_id: str | None = Field( + default=None, + description="CVE ID(s) if the jira issue is a CVE; may contain multiple CVE IDs", + ) upstream_patches: list[str] = Field( description="List of URLs to upstream patches that were validated using the get_patch_from_url tool" ) @@ -241,7 +244,10 @@ class BackportData(BaseModel): "Do NOT repeat the justification rationale here.", ) jira_issue: str = Field(description="Jira issue identifier") - cve_id: str | None = Field(description="CVE identifier", default=None) + cve_id: str | None = Field( + description="CVE identifier(s); include ALL CVE IDs when the issue covers multiple CVEs", + default=None, + ) fix_version: str | None = Field(description="Fix version in Jira (e.g., 'rhel-9.8')", default=None) diff --git a/ymir/tools/unprivileged/tests/unit/test_wicked_git.py b/ymir/tools/unprivileged/tests/unit/test_wicked_git.py index 6c4adf6c3..45827d98f 100644 --- a/ymir/tools/unprivileged/tests/unit/test_wicked_git.py +++ b/ymir/tools/unprivileged/tests/unit/test_wicked_git.py @@ -203,6 +203,16 @@ async def test_git_patch_creation_tool_with_hideous_patch_file(git_repo, tmp_pat ("CVE-2025-12346", "", "No matches found for 'CVE-2025-12346'"), ("", "RHEL-123456", "Found 1 matching commit(s) for 'RHEL-123456'"), ("", "RHEL-123457", "No matches found for 'RHEL-123457'"), + ( + "CVE-2025-12345 CVE-2025-99999", + "", + "Found 1 matching commit(s) for 'CVE-2025-12345, CVE-2025-99999'", + ), + ( + "CVE-2025-99998, CVE-2025-99999", + "", + "No matches found for 'CVE-2025-99998, CVE-2025-99999'", + ), ], ) async def test_git_log_search_tool_found(git_repo, cve_id, jira_issue, expected): diff --git a/ymir/tools/unprivileged/wicked_git.py b/ymir/tools/unprivileged/wicked_git.py index 18b1f823c..864995633 100644 --- a/ymir/tools/unprivileged/wicked_git.py +++ b/ymir/tools/unprivileged/wicked_git.py @@ -12,6 +12,10 @@ from ymir.common.version_utils import parse_branch_name from ymir.tools.base import CloneableTool as Tool +# Pattern to extract CVE IDs. Needs to handles space-separated, comma-separated, +# or any other separator as there is no standard in how those can be provided. +CVE_ID_PATTERN = re.compile(r"CVE-\d{4}-\d{4,}") + class GitPreparePackageSourcesInput(BaseModel): unpacked_sources_path: AbsolutePath = Field( @@ -541,7 +545,7 @@ async def _run( class GitLogSearchToolInput(BaseModel): repository_path: AbsolutePath = Field(description="Absolute path to the git repository") - cve_id: str = Field(description="CVE ID to look for in git history") + cve_id: str = Field(description="CVE ID(s) to look for in git history (may contain multiple CVE IDs)") jira_issue: str = Field(description="Jira issue to look for in git history") @@ -571,24 +575,23 @@ async def _run( if not (repo_path / ".git").exists(): raise ToolError(f"Not a git repository: {repo_path}") - search = tool_input.cve_id or tool_input.jira_issue - if not search: + cve_ids = CVE_ID_PATTERN.findall(tool_input.cve_id) if tool_input.cve_id else [] + search_terms = cve_ids or ([tool_input.jira_issue] if tool_input.jira_issue else []) + if not search_terms: raise ToolError("No search string provided, jira_issue or cve_id is required") - cmd = [ - "git", - "log", - "--no-merges", - f"--grep={search}", - "-n", - "1", - "--pretty=%s %H", - ] + cmd = ["git", "log", "--no-merges"] + for term in search_terms: + cmd.extend(["--grep", term]) + if cve_ids and tool_input.jira_issue: + cmd.extend(["--grep", tool_input.jira_issue]) + cmd.extend(["-n", "1", "--pretty=%s %H"]) exit_code, stdout, stderr = await run_subprocess(cmd, cwd=repo_path) if exit_code != 0: raise ToolError(f"Git command failed: {stderr}") + search = ", ".join(search_terms) output = (stdout or "").strip() if not output: return StringToolOutput(result=f"No matches found for '{search}'")