Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 46 additions & 16 deletions workflows/cve-fixer/.claude/commands/cve.find.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,50 @@ Report: artifacts/cve-fixer/find/cve-issues-20260226-145018.md

2. **Verify Jira Access**

Secrets may be injected by the Ambient session, a secrets manager, or an MCP server — do NOT rely solely on bash env var checks. Instead, attempt a lightweight test API call and let the response determine whether credentials are available.
**ALWAYS check for a Jira MCP server first** before attempting any curl/env var approach.

**2.1: Check for Jira MCP server (do this first, every time)**

**Do NOT use ToolSearch with generic keywords — it returns wrong results.**

Instead, look directly at the `<available-deferred-tools>` list that appears at the
top of every conversation. If you see any of these tool names listed there:
- `mcp__mcp-atlassian__jira_search`
- `mcp__mcp-atlassian__jira_get_issue`
- or any tool containing "atlassian" or "jira" in the name

Then fetch it using the `select:` syntax:

```
ToolSearch: select:mcp__mcp-atlassian__jira_search
```

Once fetched, use it directly for all Jira queries in Step 3.
Skip the curl/auth setup entirely.
Print: "✅ Using mcp__mcp-atlassian__jira_search for Jira queries"

If `mcp__session__refresh_credentials` appears in the deferred tools list, call it
first to activate workspace credentials, then re-check the list.

**The tool name is visible in `<available-deferred-tools>` at the start of the
conversation. Read that list — do not rely on ToolSearch keyword queries to discover it.**

**2.2: Fallback — curl with credentials (always attempt, even if bash says vars are unset)**

If no Jira MCP tool is available, attempt the curl auth call regardless of whether the
bash env var check shows the vars as set or not. Ambient secrets can be injected at the
curl level even when not visible to shell variable checks — the only reliable test is the
actual API call response.

```bash
JIRA_BASE_URL="https://redhat.atlassian.net"
AUTH=$(echo -n "${JIRA_EMAIL}:${JIRA_API_TOKEN}" | base64)

# Retry once on network failure (curl exit code 000 = timeout/no response)
# Diagnostic only — do NOT stop if these are "no"
echo "JIRA_API_TOKEN in bash env: $([ -n "${JIRA_API_TOKEN}" ] && echo yes || echo no)"
echo "JIRA_EMAIL in bash env: $([ -n "${JIRA_EMAIL}" ] && echo yes || echo no)"
echo "Attempting Jira API call regardless..."

for ATTEMPT in 1 2; do
TEST_RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -X GET \
--connect-timeout 10 --max-time 15 \
Expand All @@ -74,29 +111,22 @@ Report: artifacts/cve-fixer/find/cve-issues-20260226-145018.md
```

- **HTTP 200** → credentials valid, proceed
- **HTTP 401** → credentials missing or invalid. Note: `/rest/api/3/myself` returns 401 for all authentication failures — there is no separate 403 for this endpoint. Only now inform the user:
- Check if `JIRA_API_TOKEN` and `JIRA_EMAIL` are configured as Ambient session secrets
- If not, generate a token at https://id.atlassian.com/manage-profile/security/api-tokens and export:

```bash
export JIRA_API_TOKEN="your-token-here"
export JIRA_EMAIL="your-email@redhat.com"
```
- **HTTP 000 after retry** → persistent network issue — inform user and stop

**Do NOT pre-check env vars with `[ -z "$JIRA_API_TOKEN" ]` and stop.** The variables may be available to the API call even if not visible to the shell check (e.g. Ambient secrets injection).
- **HTTP 401** → credentials truly not available or expired. Only now stop and inform user:
configure `JIRA_API_TOKEN` and `JIRA_EMAIL` as Ambient workspace secrets or export them
- **HTTP 000 after retry** → network issue — inform user and stop

3. **Query Jira for CVE Issues**

a. Set up variables (AUTH already set from Step 2):
a. Set up variables:

```bash
COMPONENT_NAME="[from step 1]"
JIRA_BASE_URL="https://redhat.atlassian.net"
# AUTH already constructed in Step 2 — reuse it
# If using MCP (Step 2.1): pass JQL directly to MCP tool — no AUTH needed
# If using curl (Step 2.2): AUTH already constructed in Step 2 — reuse it
```

b. Construct JQL query and execute API call:
b. Construct JQL query and execute via MCP or curl:

```bash
# Normalize component name with case-insensitive lookup against mapping file
Expand Down
209 changes: 188 additions & 21 deletions workflows/cve-fixer/.claude/commands/cve.fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
## Purpose
Implement secure remediations for prioritized CVEs through dependency updates, patches, code changes, or compensating controls. This command executes the actual fixes that eliminate vulnerabilities.

## Mandatory Execution Rule

**EVERY step in the Process section below MUST be executed in order. NO step may be skipped, abbreviated, or assumed complete without actually running it.**

If a step produces no output (e.g., no `.cve-fix/` folder found, no tests discovered), log that result explicitly and continue. Do not silently skip any step.

## Execution Style

**Be concise. Brief status + final summary only.**
Expand Down Expand Up @@ -33,6 +39,11 @@ Summary:

1. **Load CVEs from Find Output or User-Specified Jira Issue**

**Supported flags:**
- `--automerge` — After creating each PR, enable GitHub's automerge so the PR merges automatically once all required checks pass. Off by default — the user must explicitly opt in.

Parse this flag before processing Jira issues. Store as `AUTOMERGE=true/false`.

**Option A: User specifies one or more Jira issues**
- If user provides one or more Jira issue IDs (e.g., `/cve.fix RHOAIENG-4973` or `/cve.fix RHOAIENG-4973 RHOAIENG-5821`):
- Process each issue independently -- extract CVE ID and component from each
Expand Down Expand Up @@ -202,15 +213,28 @@ Summary:

If `PUSH_ACCESS` is `false` or the push fails:
```bash
# Create a fork under the authenticated user's account
gh repo fork "$REPO_FULL" --clone=false

FORK_USER=$(gh api user --jq '.login')
FORK_REPO="${FORK_USER}/${REPO_NAME}"

# Add fork as a remote
# Create fork if it doesn't exist yet
gh repo fork "$REPO_FULL" --clone=false 2>/dev/null || true

# Sync fork branches with upstream — REQUIRED to ensure fix branches
# are based on current upstream code, not a stale fork.
# This syncs all branches in the fork with the upstream repo.
echo "Syncing fork ${FORK_REPO} with upstream ${REPO_FULL}..."
gh repo sync "${FORK_REPO}" --source "$REPO_FULL" --branch "$TARGET_BRANCH"

if [ $? -ne 0 ]; then
echo "⚠️ Fork sync failed — fix branch may be based on stale code. Proceeding with caution."
else
echo "✅ Fork branch ${TARGET_BRANCH} synced with upstream"
fi

# Add fork as a remote and fetch the synced branch
cd "$REPO_DIR"
git remote add fork "https://github.com/${FORK_REPO}.git"
git remote add fork "https://github.com/${FORK_REPO}.git" 2>/dev/null || true
git fetch fork "$TARGET_BRANCH"

# Push fix branch to fork, PR targets the original repo
git push fork "$FIX_BRANCH"
Expand Down Expand Up @@ -258,11 +282,34 @@ Summary:
```

4.5. **Load Global Fix Guidance from `.cve-fix/` Folder**
- Runs ONCE after all repos are cloned, BEFORE any fixes. Builds a global knowledge base from `.cve-fix/` folders across all cloned repos.
- Check every cloned repo for `.cve-fix/`, read ALL files (e.g., `examples.md`, `config.json`, `dependencies.md`, `pitfalls.md`), and merge into a single context.
- Extract: dependency co-upgrades, lock file requirements, version upgrade patterns, branch-specific instructions, known working fix versions, testing requirements, common pitfalls.
- Apply this guidance to ALL subsequent steps (5-11). Guidance from any repo applies globally; when conflicts exist, prefer the repo-specific instruction.
- If no `.cve-fix/` folders exist, proceed with default strategy.

**CRITICAL — MANDATORY. Do NOT skip. Must run before any fix is applied.**

Runs ONCE after all repos are cloned, BEFORE any fixes. Check every cloned repo for
a `.cve-fix/` directory and read ALL files inside it. This guidance contains
repo-specific fix patterns, known working versions, and pitfalls that directly affect
whether the fix will succeed.

```bash
for REPO_DIR in "${ALL_CLONED_REPOS[@]}"; do
CVE_FIX_DIR="${REPO_DIR}/.cve-fix"
if [ -d "$CVE_FIX_DIR" ]; then
echo "📖 Reading fix guidance from ${REPO_DIR}/.cve-fix/"
for FILE in "$CVE_FIX_DIR"/*; do
echo " Reading: $FILE"
cat "$FILE"
done
else
echo "ℹ️ No .cve-fix/ folder in ${REPO_DIR} — using default strategy"
fi
done
```

- Extract: dependency co-upgrades, lock file requirements, version upgrade patterns,
branch-specific instructions, known working fix versions, testing requirements, pitfalls
- Apply this guidance to ALL subsequent steps (5-11)
- When conflicts exist between repos, prefer the repo-specific instruction
- **Always log the result** — either "guidance loaded from X" or "no .cve-fix/ found in any repo"

---
> **Steps 5-11 repeat for EACH repository identified in Step 3.**
Expand Down Expand Up @@ -565,19 +612,40 @@ This issue can be closed as 'Not a Bug / ${VEX_JUSTIFICATION}' if the above evid

**How to check:**

Two searches are needed — bots like Dependabot and Renovate open PRs that fix the same
vulnerability without mentioning the CVE ID, only the package name and version bump.

```bash
REPO_FULL="opendatahub-io/models-as-a-service" # org/repo from mapping
CVE_ID="CVE-YYYY-XXXXX"
PACKAGE="urllib3" # extracted from Jira summary in Step 1
TARGET_BRANCH="main" # from mapping or user input

# Search open PRs for this specific CVE ID targeting this branch
EXISTING_PR=$(gh pr list --repo "$REPO_FULL" --state open --base "$TARGET_BRANCH" --search "$CVE_ID" --json number,title,url,headRefName,baseRefName --jq '.[0]' 2>/dev/null)
EXISTING_PR=""

# Search 1: by CVE ID (catches our own PRs and manually created ones)
EXISTING_PR=$(gh pr list --repo "$REPO_FULL" --state open --base "$TARGET_BRANCH" \
--search "$CVE_ID" --json number,title,url --jq '.[0]' 2>/dev/null)

# Search 2: by package name (catches Dependabot/Renovate PRs that don't mention CVE ID)
if [ -z "$EXISTING_PR" ] || [ "$EXISTING_PR" = "null" ]; then
EXISTING_PR=$(gh pr list --repo "$REPO_FULL" --state open --base "$TARGET_BRANCH" \
--search "$PACKAGE" --json number,title,url,author \
--jq '[.[] | select(.author.login | test("dependabot|renovate|renovate-bot"; "i"))] | .[0]' \
2>/dev/null)

# If no bot PR, still check for any open PR bumping this package
# (a human may have already opened a fix PR without mentioning the CVE)
if [ -z "$EXISTING_PR" ] || [ "$EXISTING_PR" = "null" ]; then
EXISTING_PR=$(gh pr list --repo "$REPO_FULL" --state open --base "$TARGET_BRANCH" \
--search "$PACKAGE" --json number,title,url --jq '.[0]' 2>/dev/null)
fi
Comment on lines +638 to +641
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Simulate broad package search to assess false positive risk.

# Example: search for common package names in a sample repo
REPO_FULL="opendatahub-io/odh-dashboard"
COMMON_PACKAGES=("config" "utils" "client" "core" "common")

for PKG in "${COMMON_PACKAGES[@]}"; do
  echo "=== Searching for package: $PKG ==="
  gh pr list --repo "$REPO_FULL" --state open \
    --search "$PKG" --json number,title --jq '.[] | "\(.number): \(.title)"' 2>/dev/null | head -5
  echo ""
done

Repository: ambient-code/workflows

Length of output: 257


🏁 Script executed:

#!/bin/bash
# Read the file and check its size
wc -l "workflows/cve-fixer/.claude/commands/cve.fix.md"

Repository: ambient-code/workflows

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Read the relevant section (lines 613-690 as mentioned in scratchpad)
sed -n '613,690p' "workflows/cve-fixer/.claude/commands/cve.fix.md"

Repository: ambient-code/workflows

Length of output: 4118


🏁 Script executed:

#!/bin/bash
# Search for where PACKAGE is extracted/defined
rg -n "PACKAGE=" "workflows/cve-fixer/.claude/commands/cve.fix.md" | head -20

Repository: ambient-code/workflows

Length of output: 200


🏁 Script executed:

#!/bin/bash
# Check the Step 1 section to understand PACKAGE extraction
sed -n '1,612p' "workflows/cve-fixer/.claude/commands/cve.fix.md" | tail -100

Repository: ambient-code/workflows

Length of output: 5814


Search 3's broad package matching is by design, but consider documenting its false-positive trade-off.

The three-stage search strategy at lines 638-641 is intentional: Search 3 uses --search "$PACKAGE" without additional filtering to catch human-opened PRs that may already address the CVE without mentioning it explicitly (e.g., "Bump urllib3 from 1.26.5 to 2.2.3"). This is documented in the comments at lines 684-686.

However, the trade-off is real: for package names that are common words ("client", "utils", "config"), the gh pr list --search command can match unrelated PRs whose titles or bodies happen to contain these words. The jq filter .[0] then selects the first match, which may not actually be about updating the package.

Rather than tightening the search with word boundaries or keyword filters (which would change the design intent), consider:

  1. Adding a comment in the code explicitly stating this false-positive trade-off and why it's acceptable (better to skip a CVE if someone may be working on it than to create a duplicate)
  2. Documenting that operators should review matched PRs when package names are common words
  3. Accepting this as a known limitation of the approach when PACKAGE values are generic
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workflows/cve-fixer/.claude/commands/cve.fix.md` around lines 638 - 641, Add
an inline comment next to the EXISTING_PR lookup (the gh pr list invocation
using --search "$PACKAGE" and the jq '.[0]' selection) that documents the known
false-positive trade-off: explain that Search 3 intentionally performs a broad
search to catch human-opened PRs that may not mention the CVE, that this can
match generic package names (e.g., "client", "utils", "config") and return
unrelated PRs, and instruct operators to manually review matches for such common
PACKAGE values; state this limitation is accepted to avoid creating duplicate
PRs.

fi
Comment on lines +639 to +642
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Package-name fallback can skip unrelated PRs on common package terms.

At Line 597-Line 599, the third search picks the first open PR matching $PACKAGE on the target branch. For common names, this can create false positives and incorrectly skip required CVE fixes.

Proposed tightening
-EXISTING_PR=$(gh pr list --repo "$REPO_FULL" --state open --base "$TARGET_BRANCH" \
-  --search "$PACKAGE" --json number,title,url --jq '.[0]' 2>/dev/null)
+EXISTING_PR=$(gh pr list --repo "$REPO_FULL" --state open --base "$TARGET_BRANCH" \
+  --search "$PACKAGE" --json number,title,url \
+  --jq '[.[] | select(.title | test("(^|[^A-Za-z0-9])" + $pkg + "([^A-Za-z0-9]|$)"; "i"))
+              | select(.title | test("bump|upgrade|security|cve"; "i"))] | .[0]' \
+  --arg pkg "$PACKAGE" 2>/dev/null)

Also applies to: 642-647

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workflows/cve-fixer/.claude/commands/cve.fix.md` around lines 597 - 600, The
EXISTING_PR lookup uses a loose gh pr list --search "$PACKAGE" which can match
unrelated PRs for common package names; update the EXISTING_PR query (the gh pr
list invocation that writes to EXISTING_PR) to tighten matching by requiring the
package name appear as an exact word in the PR title or by additionally
filtering for a security/CVE context (e.g., require "CVE" or "security" in the
title/body) using the gh --jq filter so false positives are avoided; apply the
same fix to the second occurrence of the gh pr list that appears around the
642–647 region so both searches use the stricter filter referencing $PACKAGE,
TARGET_BRANCH and REPO_FULL.


if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
PR_NUMBER=$(echo "$EXISTING_PR" | jq -r '.number')
PR_TITLE=$(echo "$EXISTING_PR" | jq -r '.title')
PR_URL=$(echo "$EXISTING_PR" | jq -r '.url')
echo "⏭️ Skipping $CVE_ID — existing open PR found:"
echo "⏭️ Skipping $CVE_ID — existing open PR found (may be Dependabot/Renovate):"
echo " PR #${PR_NUMBER}: ${PR_TITLE}"
echo " URL: ${PR_URL}"

Expand Down Expand Up @@ -613,11 +681,13 @@ This issue can be closed as 'Not a Bug / ${VEX_JUSTIFICATION}' if the above evid
- Proceed with the fix (Step 6 onwards)

**Search strategy:**
- Search by exact CVE ID first (e.g., `CVE-2025-61726`) — this is the most reliable match
- **Search 1 — by CVE ID**: catches our own PRs and any manually created ones
- **Search 2 — by package name filtered to bots**: catches Dependabot/Renovate PRs that bump the vulnerable package without mentioning the CVE ID (e.g. "Bump urllib3 from 1.26.5 to 2.2.3")
- **Search 3 — by package name broadly**: catches any human-opened PR for the same package, regardless of author
- The `gh pr list --search` command searches PR titles and bodies
- A single PR may address multiple CVEs (e.g., "fix: cve-2025-61726 and cve-2025-68121") — if ANY of the target CVEs appear in an existing PR, consider all CVEs in that PR as already handled
- **IMPORTANT: Only skip for OPEN PRs.** Closed or merged PRs should be ignored — if a previous PR was closed without merging (e.g., it was incorrect or superseded), it is valid to create a new fix PR for the same CVE. The `--state open` flag in the `gh pr list` command ensures only open PRs are checked.
- **Stale remote branches**: A previous automation run may have left a remote branch with the same name (e.g., `fix/cve-YYYY-XXXXX-attempt-1`) from a closed PR. If push fails due to a conflicting remote branch, increment the attempt number (e.g., `attempt-2`) or delete the stale remote branch with `git push origin --delete <branch-name>` before pushing.
- A single PR may address multiple CVEs — if any of the target CVEs or the affected package appears in an existing open PR, skip creating a duplicate
- **IMPORTANT: Only skip for OPEN PRs.** Closed or merged PRs should be ignored — it is valid to create a new fix PR for the same CVE if a previous PR was closed without merging. The `--state open` flag ensures only open PRs are checked.
- **Stale remote branches**: If push fails due to a conflicting remote branch from a previous run, increment the attempt number (e.g., `attempt-2`) or delete the stale branch with `git push origin --delete <branch-name>`.

**Example output when PR exists:**
```
Expand Down Expand Up @@ -982,10 +1052,92 @@ This issue can be closed as 'Not a Bug / ${VEX_JUSTIFICATION}' if the above evid
**Full test log**: `artifacts/cve-fixer/fixes/test-results/test-run-20260218-143022.log`
```

10.5. **Post-Fix CVE Verification**

**CRITICAL — This step is MANDATORY. Do NOT skip it. Do NOT proceed to PR creation without completing this step.**

Source-level scanning (Step 5) checks the dependency manifests before the fix.
This step verifies the fix actually worked by scanning the **compiled output** after
the fix is applied. A source scan can give false negatives (e.g., go.mod updated but
toolchain not recompiled, or a transitive dep overrides the fixed version at build time).

**For Go projects (preferred — binary scan):**

```bash
echo "Building and scanning binary to verify fix for CVE-${CVE_ID}..."
cd "$REPO_DIR"

# Build the binary with the fixed go.mod
go build -o /tmp/fixed-binary-${REPO_NAME} ./... 2>&1

if [ $? -eq 0 ]; then
# Scan the compiled binary — this is the gold standard
POST_SCAN_OUTPUT=$(GOTOOLCHAIN="go${TARGET_GO_VERSION}" \
govulncheck -mode binary /tmp/fixed-binary-${REPO_NAME} 2>&1)
rm -f /tmp/fixed-binary-${REPO_NAME}
else
echo "⚠️ Binary build failed — falling back to source scan"
POST_SCAN_OUTPUT=$(GOTOOLCHAIN="go${TARGET_GO_VERSION}" govulncheck -show verbose ./... 2>&1)
fi
```

**For Python projects:**

```bash
# Re-run pip-audit on the modified requirements file
POST_SCAN_OUTPUT=$(pip-audit -r requirements.txt 2>&1)
```

**For Node.js projects:**

```bash
# Re-run npm audit after package.json changes
npm install --package-lock-only 2>/dev/null # regenerate lockfile
POST_SCAN_OUTPUT=$(npm audit --json 2>&1)
```

**Check result:**

```bash
if echo "$POST_SCAN_OUTPUT" | grep -q "$CVE_ID"; then
CVE_STILL_PRESENT=true
else
CVE_STILL_PRESENT=false
fi
```
Comment on lines +1093 to +1107
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Post-fix verification can produce false passes when scan command fails.

At Line 1023, the command is executed without checking exit status, and the decision at Line 1027 only greps output for $CVE_ID. If govulncheck fails (tool missing/network/toolchain issue), this can incorrectly set CVE_STILL_PRESENT=false and allow PR creation.

Proposed fix
-POST_SCAN_OUTPUT=$(GOTOOLCHAIN="go${TARGET_GO_VERSION}" govulncheck -show verbose ./... 2>&1)
-# For Python: pip-audit -r requirements.txt 2>/dev/null
-# For Node:   npm audit --json 2>/dev/null
-
-if echo "$POST_SCAN_OUTPUT" | grep -q "$CVE_ID"; then
+POST_SCAN_OUTPUT=""
+POST_SCAN_EXIT=0
+
+if [ -f "go.mod" ]; then
+  POST_SCAN_OUTPUT=$(GOTOOLCHAIN="go${TARGET_GO_VERSION}" govulncheck -show verbose ./... 2>&1)
+  POST_SCAN_EXIT=$?
+elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
+  POST_SCAN_OUTPUT=$(pip-audit -r requirements.txt 2>&1)
+  POST_SCAN_EXIT=$?
+elif [ -f "package.json" ]; then
+  POST_SCAN_OUTPUT=$(npm audit --json 2>&1)
+  POST_SCAN_EXIT=$?
+else
+  echo "⚠️ Could not determine scanner for repository; skipping PR creation for safety."
+  CVE_STILL_PRESENT=true
+fi
+
+if [ "${POST_SCAN_EXIT}" -ne 0 ]; then
+  echo "⚠️ Post-fix scan failed (exit ${POST_SCAN_EXIT}); skipping PR creation for safety."
+  CVE_STILL_PRESENT=true
+elif echo "$POST_SCAN_OUTPUT" | grep -q "$CVE_ID"; then
   CVE_STILL_PRESENT=true
 else
   CVE_STILL_PRESENT=false
 fi

Also applies to: 1036-1037

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workflows/cve-fixer/.claude/commands/cve.fix.md` around lines 1020 - 1032,
The post-fix scan captures tool output into POST_SCAN_OUTPUT but never checks
the scan command status, so a failed govulncheck (or pip-audit/npm audit) can
produce an empty output and falsely mark CVE_STILL_PRESENT=false; update the
block that runs govulncheck (GOTOOLCHAIN="go${TARGET_GO_VERSION}" govulncheck
...) to capture its exit code ($?) immediately and if non-zero either fail the
workflow or set CVE_STILL_PRESENT=true and emit a clear error via echo/process
logger; then only set CVE_STILL_PRESENT=false when the scan succeeded and grep
of POST_SCAN_OUTPUT for $CVE_ID returns no match (apply the same exit-code check
pattern for the Python/Node variants).


**If CVE is gone** (`CVE_STILL_PRESENT=false`) → proceed to PR creation ✅

**If CVE is still present** (`CVE_STILL_PRESENT=true`) → **do NOT create a PR**:
- Print: "❌ CVE-YYYY-XXXXX still detected after fix attempt in [repo] ([branch]). Fix was insufficient."
- Add a Jira comment:

```bash
COMMENT_TEXT="Automated fix attempted but CVE still detected after applying changes.

Fix attempted: ${FIX_DESCRIPTION}
Post-fix scan: CVE still present in ${REPO_FULL} on branch ${TARGET_BRANCH}
Scan date: $(date -u +%Y-%m-%dT%H:%M:%SZ)

Manual investigation required — the automated fix did not resolve this CVE.
Possible causes: transitive dependency conflict, incorrect package targeted, or
the fix requires additional changes beyond a version bump."

COMMENT_JSON=$(jq -n --arg body "$COMMENT_TEXT" '{"body": $body}')
AUTH=$(echo -n "${JIRA_EMAIL}:${JIRA_API_TOKEN}" | base64)
curl -s -X POST \
-H "Authorization: Basic ${AUTH}" \
-H "Content-Type: application/json" \
-d "$COMMENT_JSON" \
"${JIRA_BASE_URL}/rest/api/3/issue/${JIRA_KEY}/comment"
```
- Document in `artifacts/cve-fixer/fixes/fix-failed-CVE-YYYY-XXXXX.md`
- Skip to next CVE/branch — do not create PR for this one

11. **Create Pull Requests**
- **CRITICAL**: You MUST actually CREATE the PRs using `gh pr create` command
- **CRITICAL**: Create a SEPARATE PR for EACH CVE (NOT combined)
- **CRITICAL**: Only create PRs for CVEs that were ACTUALLY FIXED (not for CVEs that were already fixed in Step 5)
- **CRITICAL**: Only create PRs for CVEs that passed post-fix verification in Step 10.5 (not for CVEs that were already fixed in Step 5 or where the fix was insufficient)
- For each CVE fix that was successfully committed and pushed:
- Generate PR title: `Security: Fix CVE-YYYY-XXXXX (<package-name>)`
- **Extract Jira issue IDs for this CVE:**
Expand Down Expand Up @@ -1074,10 +1226,16 @@ This PR fixes **CVE-YYYY-XXXXX** by upgrading <package> from X.X.X to Y.Y.Y.
EOF
)

gh pr create \
PR_URL=$(gh pr create \
--base <target-branch> \
--title "Security: Fix CVE-YYYY-XXXXX (<package-name>)" \
--body "$PR_BODY"
--body "$PR_BODY")

# Enable automerge if --automerge flag was passed
if [ "$AUTOMERGE" = "true" ]; then
gh pr merge --auto --squash "$PR_URL"
echo "✅ Automerge enabled on $PR_URL — will merge when checks pass"
fi
Comment on lines +1229 to +1238
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a guard before enabling automerge to avoid acting on an empty PR URL.

At Line 1082 and Line 1088, automerge is attempted based only on AUTOMERGE. Add a non-empty PR_URL check first so failure paths are explicit and don’t continue with invalid input.

Suggested fix
-       PR_URL=$(gh pr create \
+       PR_URL=$(gh pr create \
          --base <target-branch> \
          --title "Security: Fix CVE-YYYY-XXXXX (<package-name>)" \
          --body "$PR_BODY")
 
        # Enable automerge if --automerge flag was passed
-       if [ "$AUTOMERGE" = "true" ]; then
+       if [ -z "$PR_URL" ]; then
+         echo "❌ PR creation did not return a URL; skipping automerge"
+       elif [ "$AUTOMERGE" = "true" ]; then
          gh pr merge --auto --squash "$PR_URL"
          echo "✅ Automerge enabled on $PR_URL — will merge when checks pass"
        fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PR_URL=$(gh pr create \
--base <target-branch> \
--title "Security: Fix CVE-YYYY-XXXXX (<package-name>)" \
--body "$PR_BODY"
--body "$PR_BODY")
# Enable automerge if --automerge flag was passed
if [ "$AUTOMERGE" = "true" ]; then
gh pr merge --auto --squash "$PR_URL"
echo "✅ Automerge enabled on $PR_URL — will merge when checks pass"
fi
PR_URL=$(gh pr create \
--base <target-branch> \
--title "Security: Fix CVE-YYYY-XXXXX (<package-name>)" \
--body "$PR_BODY")
# Enable automerge if --automerge flag was passed
if [ -z "$PR_URL" ]; then
echo "❌ PR creation did not return a URL; skipping automerge"
elif [ "$AUTOMERGE" = "true" ]; then
gh pr merge --auto --squash "$PR_URL"
echo "✅ Automerge enabled on $PR_URL — will merge when checks pass"
fi
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 1082-1082: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@workflows/cve-fixer/.claude/commands/cve.fix.md` around lines 1082 - 1091,
Add a guard that verifies PR_URL is non-empty before attempting automerge:
instead of only testing AUTOVERGE, update the conditional around gh pr merge to
require both AUTOMERGE="true" and a non-empty PR_URL (e.g., test -n "$PR_URL"),
and if PR_URL is empty emit an error message and skip/exit rather than calling
gh pr merge with an invalid URL; update the block that references PR_URL,
AUTOMERGE, and the gh pr merge command accordingly.

```
- Capture the PR URL from the command output
- Save PR URL to fix implementation report
Expand Down Expand Up @@ -1155,6 +1313,9 @@ EOF
- PR URL for the created pull request

- **Already Fixed Report**: `artifacts/cve-fixer/fixes/already-fixed-CVE-YYYY-XXXXX.md` (if CVE confirmed not present via both scan and package check)

- **Fix Failed Report**: `artifacts/cve-fixer/fixes/fix-failed-CVE-YYYY-XXXXX.md` (if post-fix re-scan still detects the CVE)
- Fix attempted, post-fix scan output, Jira comment added, manual review required
- CVE ID, repository, and scan evidence

- **VEX Justified Report**: `artifacts/cve-fixer/fixes/vex-justified-CVE-YYYY-XXXXX.md` (if auto-detected VEX justification added to Jira)
Expand Down Expand Up @@ -1222,6 +1383,12 @@ Fix multiple specific Jira issues:
/cve.fix RHOAIENG-4973 RHOAIENG-5821
```

Fix and enable automerge on all created PRs (merges automatically when checks pass):
```
/cve.fix --automerge
/cve.fix RHOAIENG-4973 --automerge
```

Fix with custom message:
```
/cve.fix Fix open CVEs found in latest scan
Expand Down
Loading
Loading