|
| 1 | +# ============================================================================= |
| 2 | +# CVE Remediation Controller - CodeFlare Operator |
| 3 | +# ============================================================================= |
| 4 | +# This workflow scans the codeflare-operator repository for a specific CVE and |
| 5 | +# creates a Renovate PR to fix the vulnerable dependency. |
| 6 | +# |
| 7 | +# Target: red-hat-data-services/codeflare-operator (downstream only) |
| 8 | +# Since codeflare-operator is only shipped in older versions, we only need to |
| 9 | +# fix downstream release branches, not main/midstream. |
| 10 | +# |
| 11 | +# Usage: |
| 12 | +# 1. Create a GitHub issue to track the CVE |
| 13 | +# 2. Trigger this workflow with: |
| 14 | +# - cve_id: The CVE to check for (e.g., CVE-2024-12345) |
| 15 | +# - issue_number: The tracking issue number |
| 16 | +# - release_tag: The release to check (e.g., v1.14.0) |
| 17 | +# 3. The workflow will: |
| 18 | +# - Create a fix branch: cve-fix/<cve-id>-<release-tag> |
| 19 | +# - Scan the release for the CVE using govulncheck |
| 20 | +# - If not affected: comment and close the issue |
| 21 | +# - If affected: create a Renovate PR to fix the dependency |
| 22 | +# |
| 23 | +# PAT Requirements: |
| 24 | +# The CVE_CONTROLLER_PAT secret needs repo scope for PR creation |
| 25 | +# ============================================================================= |
| 26 | + |
| 27 | +name: CVE Remediation Controller |
| 28 | + |
| 29 | +on: |
| 30 | + workflow_dispatch: |
| 31 | + inputs: |
| 32 | + cve_id: |
| 33 | + description: 'CVE identifier (e.g., CVE-2024-12345)' |
| 34 | + required: true |
| 35 | + type: string |
| 36 | + issue_number: |
| 37 | + description: 'GitHub Issue ID to update with status' |
| 38 | + required: true |
| 39 | + type: string |
| 40 | + release_tag: |
| 41 | + description: 'Release tag to base the fix branch from (e.g., v1.14.0)' |
| 42 | + required: true |
| 43 | + type: string |
| 44 | + # ------------------------------------------------------------------------- |
| 45 | + # Override inputs for testing with forks |
| 46 | + # ------------------------------------------------------------------------- |
| 47 | + repo_override: |
| 48 | + description: 'Override target repo for testing (e.g., your-username/codeflare-operator)' |
| 49 | + required: false |
| 50 | + type: string |
| 51 | + issue_repo_override: |
| 52 | + description: 'Override issue repo for testing (e.g., your-username/codeflare-operator)' |
| 53 | + required: false |
| 54 | + type: string |
| 55 | + |
| 56 | +# Ensure only one CVE remediation runs at a time per CVE/release combination |
| 57 | +concurrency: |
| 58 | + group: cve-operator-${{ github.event.inputs.cve_id }}-${{ github.event.inputs.release_tag }} |
| 59 | + cancel-in-progress: false |
| 60 | + |
| 61 | +env: |
| 62 | + # Default repository - downstream only for codeflare-operator |
| 63 | + TARGET_REPO: ${{ inputs.repo_override || 'red-hat-data-services/codeflare-operator' }} |
| 64 | + ISSUE_REPO: ${{ inputs.issue_repo_override || inputs.repo_override || 'red-hat-data-services/codeflare-operator' }} |
| 65 | + |
| 66 | +jobs: |
| 67 | + # =========================================================================== |
| 68 | + # Job 1: Scan for CVE |
| 69 | + # =========================================================================== |
| 70 | + scan: |
| 71 | + name: Scan for CVE |
| 72 | + runs-on: ubuntu-latest |
| 73 | + outputs: |
| 74 | + vulnerable: ${{ steps.scan.outputs.vulnerable }} |
| 75 | + package_name: ${{ steps.scan.outputs.package_name }} |
| 76 | + fix_branch: ${{ steps.branch.outputs.fix_branch }} |
| 77 | + |
| 78 | + steps: |
| 79 | + # ----------------------------------------------------------------------- |
| 80 | + # Step 1: Generate branch name from CVE and release tag |
| 81 | + # Format: cve-fix/<cve-id>-<release-tag> |
| 82 | + # ----------------------------------------------------------------------- |
| 83 | + - name: Generate fix branch name |
| 84 | + id: branch |
| 85 | + run: | |
| 86 | + # Create branch name: cve-fix/CVE-2024-12345-v1.14.0 |
| 87 | + CVE_ID="${{ inputs.cve_id }}" |
| 88 | + RELEASE_TAG="${{ inputs.release_tag }}" |
| 89 | +
|
| 90 | + # Sanitize for branch name (lowercase, replace invalid chars) |
| 91 | + FIX_BRANCH="cve-fix/${CVE_ID}-${RELEASE_TAG}" |
| 92 | + FIX_BRANCH=$(echo "$FIX_BRANCH" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9./-]/-/g') |
| 93 | +
|
| 94 | + echo "fix_branch=${FIX_BRANCH}" >> $GITHUB_OUTPUT |
| 95 | + echo "Generated fix branch: ${FIX_BRANCH}" |
| 96 | +
|
| 97 | + # ----------------------------------------------------------------------- |
| 98 | + # Step 2: Create branch from release tag |
| 99 | + # ----------------------------------------------------------------------- |
| 100 | + - name: Create fix branch from release tag |
| 101 | + run: | |
| 102 | + FIX_BRANCH="${{ steps.branch.outputs.fix_branch }}" |
| 103 | + RELEASE_TAG="${{ inputs.release_tag }}" |
| 104 | +
|
| 105 | + echo "Creating branch ${FIX_BRANCH} from tag ${RELEASE_TAG}" |
| 106 | +
|
| 107 | + git clone https://x-access-token:${{ secrets.CVE_CONTROLLER_PAT }}@github.com/${{ env.TARGET_REPO }}.git repo |
| 108 | + cd repo |
| 109 | +
|
| 110 | + git config user.email "[email protected]" |
| 111 | + git config user.name "CVE Controller" |
| 112 | +
|
| 113 | + # Check if branch already exists |
| 114 | + if git ls-remote --heads origin "${FIX_BRANCH}" | grep -q .; then |
| 115 | + echo "Branch ${FIX_BRANCH} already exists, will use existing branch" |
| 116 | + else |
| 117 | + echo "Creating new branch from tag ${RELEASE_TAG}" |
| 118 | + git fetch --tags |
| 119 | + git checkout tags/${RELEASE_TAG} -b "${FIX_BRANCH}" |
| 120 | + git push origin "${FIX_BRANCH}" |
| 121 | + fi |
| 122 | +
|
| 123 | + # ----------------------------------------------------------------------- |
| 124 | + # Step 3: Checkout the fix branch |
| 125 | + # ----------------------------------------------------------------------- |
| 126 | + - name: Checkout fix branch |
| 127 | + uses: actions/checkout@v4 |
| 128 | + with: |
| 129 | + repository: ${{ env.TARGET_REPO }} |
| 130 | + ref: ${{ steps.branch.outputs.fix_branch }} |
| 131 | + token: ${{ secrets.CVE_CONTROLLER_PAT }} |
| 132 | + fetch-depth: 0 |
| 133 | + |
| 134 | + # ----------------------------------------------------------------------- |
| 135 | + # Step 4: Setup Go and install govulncheck |
| 136 | + # ----------------------------------------------------------------------- |
| 137 | + - name: Setup Go |
| 138 | + uses: actions/setup-go@v5 |
| 139 | + with: |
| 140 | + go-version-file: './go.mod' |
| 141 | + |
| 142 | + - name: Install govulncheck |
| 143 | + run: go install golang.org/x/vuln/cmd/govulncheck@latest |
| 144 | + |
| 145 | + # ----------------------------------------------------------------------- |
| 146 | + # Step 5: Scan for CVE using govulncheck |
| 147 | + # ----------------------------------------------------------------------- |
| 148 | + - name: Scan for CVE |
| 149 | + id: scan |
| 150 | + run: | |
| 151 | + set +e |
| 152 | +
|
| 153 | + CVE_ID="${{ inputs.cve_id }}" |
| 154 | + VULNERABLE="false" |
| 155 | + PACKAGE_NAME="" |
| 156 | +
|
| 157 | + echo "=== Scanning for ${CVE_ID} ===" |
| 158 | +
|
| 159 | + # Run govulncheck and capture JSON output |
| 160 | + # govulncheck automatically respects vendor directory if present |
| 161 | + # The output is streaming JSON - one JSON object per line |
| 162 | + echo "Running govulncheck..." |
| 163 | + govulncheck -json ./... > /tmp/govulncheck-output.json 2>&1 || true |
| 164 | +
|
| 165 | + echo "Full output saved to /tmp/govulncheck-output.json" |
| 166 | +
|
| 167 | + # Search for the CVE in the output |
| 168 | + # govulncheck outputs streaming JSON with 'osv' field containing OSV entries |
| 169 | + # The OSV id is typically GO-XXXX-XXXX, while CVE IDs are in aliases |
| 170 | + # We need to parse each line as a separate JSON object |
| 171 | + MATCH=$(cat /tmp/govulncheck-output.json | \ |
| 172 | + jq -c "select(.osv != null) | .osv | select(.id == \"${CVE_ID}\" or (.aliases // [] | index(\"${CVE_ID}\") != null))" 2>/dev/null | \ |
| 173 | + head -1 || echo "") |
| 174 | +
|
| 175 | + if [ -n "$MATCH" ] && [ "$MATCH" != "null" ] && [ "$MATCH" != "" ]; then |
| 176 | + VULNERABLE="true" |
| 177 | + # Extract the affected module name from the OSV record |
| 178 | + # For Go, the affected field contains module info under package.name |
| 179 | + PACKAGE_NAME=$(echo "$MATCH" | jq -r '.affected[0].package.name // "unknown"' 2>/dev/null | head -1) |
| 180 | +
|
| 181 | + # Debug output |
| 182 | + echo "Found vulnerability!" |
| 183 | + echo "OSV ID: $(echo "$MATCH" | jq -r '.id')" |
| 184 | + echo "Aliases: $(echo "$MATCH" | jq -r '.aliases // [] | join(", ")')" |
| 185 | + echo "Affected Package: $PACKAGE_NAME" |
| 186 | + fi |
| 187 | +
|
| 188 | + echo "vulnerable=${VULNERABLE}" >> $GITHUB_OUTPUT |
| 189 | + echo "package_name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT |
| 190 | + echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT |
| 191 | +
|
| 192 | + echo "=== Scan Complete. Vulnerable: ${VULNERABLE} ===" |
| 193 | +
|
| 194 | + # ----------------------------------------------------------------------- |
| 195 | + # Step 6: Report results |
| 196 | + # ----------------------------------------------------------------------- |
| 197 | + - name: Comment - Not Vulnerable |
| 198 | + if: steps.scan.outputs.vulnerable == 'false' |
| 199 | + run: | |
| 200 | + gh issue comment ${{ inputs.issue_number }} \ |
| 201 | + --repo ${{ env.ISSUE_REPO }} \ |
| 202 | + --body "## ✅ CVE Scan Result: Not Vulnerable |
| 203 | +
|
| 204 | + | Field | Value | |
| 205 | + |-------|-------| |
| 206 | + | CVE | ${{ inputs.cve_id }} | |
| 207 | + | Release Tag | ${{ inputs.release_tag }} | |
| 208 | + | Fix Branch | ${{ steps.branch.outputs.fix_branch }} | |
| 209 | + | Scan Time | ${{ steps.scan.outputs.timestamp }} | |
| 210 | +
|
| 211 | + The CVE was **not detected** in the dependencies for this release. |
| 212 | +
|
| 213 | + Closing this issue." |
| 214 | +
|
| 215 | + gh issue close ${{ inputs.issue_number }} --repo ${{ env.ISSUE_REPO }} --reason "not planned" |
| 216 | + env: |
| 217 | + GH_TOKEN: ${{ secrets.CVE_CONTROLLER_PAT }} |
| 218 | + |
| 219 | + - name: Comment - Vulnerable |
| 220 | + if: steps.scan.outputs.vulnerable == 'true' |
| 221 | + run: | |
| 222 | + gh issue comment ${{ inputs.issue_number }} \ |
| 223 | + --repo ${{ env.ISSUE_REPO }} \ |
| 224 | + --body "## ⚠️ CVE Scan Result: Vulnerable |
| 225 | +
|
| 226 | + | Field | Value | |
| 227 | + |-------|-------| |
| 228 | + | CVE | ${{ inputs.cve_id }} | |
| 229 | + | Release Tag | ${{ inputs.release_tag }} | |
| 230 | + | Fix Branch | ${{ steps.branch.outputs.fix_branch }} | |
| 231 | + | Affected Package | \`${{ steps.scan.outputs.package_name }}\` | |
| 232 | +
|
| 233 | + Triggering Renovate to create a fix PR..." |
| 234 | + env: |
| 235 | + GH_TOKEN: ${{ secrets.CVE_CONTROLLER_PAT }} |
| 236 | + |
| 237 | + # =========================================================================== |
| 238 | + # Job 2: Create Fix PR |
| 239 | + # =========================================================================== |
| 240 | + fix: |
| 241 | + name: Create Fix PR |
| 242 | + needs: scan |
| 243 | + if: needs.scan.outputs.vulnerable == 'true' |
| 244 | + runs-on: ubuntu-latest |
| 245 | + |
| 246 | + steps: |
| 247 | + - name: Checkout (for Renovate config) |
| 248 | + uses: actions/checkout@v4 |
| 249 | + |
| 250 | + - name: Run Renovate |
| 251 | + uses: renovatebot/[email protected] |
| 252 | + with: |
| 253 | + configurationFile: .github/renovate-cve-config.js |
| 254 | + token: ${{ secrets.CVE_CONTROLLER_PAT }} |
| 255 | + env: |
| 256 | + RENOVATE_TARGET_REPO: ${{ env.TARGET_REPO }} |
| 257 | + RENOVATE_BASE_BRANCH: ${{ needs.scan.outputs.fix_branch }} |
| 258 | + RENOVATE_PACKAGE_NAME: ${{ needs.scan.outputs.package_name }} |
| 259 | + RENOVATE_CVE_ID: ${{ inputs.cve_id }} |
| 260 | + RENOVATE_ISSUE_NUMBER: ${{ inputs.issue_number }} |
| 261 | + RENOVATE_ISSUE_REPO: ${{ env.ISSUE_REPO }} |
| 262 | + RENOVATE_COMPONENT: codeflare-operator |
| 263 | + RENOVATE_SCAN_TYPE: go |
| 264 | + LOG_LEVEL: debug |
| 265 | + |
| 266 | + - name: Comment - PR Created |
| 267 | + run: | |
| 268 | + gh issue comment ${{ inputs.issue_number }} \ |
| 269 | + --repo ${{ env.ISSUE_REPO }} \ |
| 270 | + --body "## 🔧 Fix PR Initiated |
| 271 | +
|
| 272 | + | Field | Value | |
| 273 | + |-------|-------| |
| 274 | + | Package | \`${{ needs.scan.outputs.package_name }}\` | |
| 275 | + | Fix Branch | ${{ needs.scan.outputs.fix_branch }} | |
| 276 | + | CVE | ${{ inputs.cve_id }} | |
| 277 | +
|
| 278 | + Renovate has been triggered. Check the repository for the new PR." |
| 279 | + env: |
| 280 | + GH_TOKEN: ${{ secrets.CVE_CONTROLLER_PAT }} |
| 281 | + |
| 282 | + - name: Comment on failure |
| 283 | + if: failure() |
| 284 | + run: | |
| 285 | + gh issue comment ${{ inputs.issue_number }} \ |
| 286 | + --repo ${{ env.ISSUE_REPO }} \ |
| 287 | + --body "## ❌ Fix PR Failed |
| 288 | +
|
| 289 | + Renovate failed to create a PR. Check the workflow logs: |
| 290 | + ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" |
| 291 | + env: |
| 292 | + GH_TOKEN: ${{ secrets.CVE_CONTROLLER_PAT }} |
0 commit comments