Create PRs to Remediate vulns #14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Create PRs to Remediate vulns | |
| # ============================================================ | |
| # This workflow fetches open Dependabot alerts from GitHub and | |
| # creates PRs for each alert with instructions for Devin AI. | |
| # | |
| # TOKEN REQUIREMENTS: | |
| # - GITHUB_TOKEN: Default token with vulnerability-alerts:read for Dependabot alerts, | |
| # and contents:write for creating branches and PRs | |
| # - DEVIN_AI_PR_BOT_SLACK_TOKEN: For Slack notifications to Devin AI | |
| # ============================================================ | |
| on: | |
| workflow_dispatch: # Allow manual triggering | |
| schedule: | |
| # Run every night at 8pm EST (1am UTC next day) | |
| - cron: "0 1 * * *" | |
| jobs: | |
| create-dependabot-prs: | |
| name: Check Dependabot alerts | |
| # Skip this job when triggered by cron schedule | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Fetch Dependabot alerts | |
| id: fetch-alerts | |
| uses: actions/github-script@v8 | |
| env: | |
| ALERTS_FILE: ${{ runner.temp }}/dependabot-alerts.json | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const alertsFile = process.env.ALERTS_FILE; | |
| // Fetch all open Dependabot alerts using the workflow's GITHUB_TOKEN | |
| let alerts = []; | |
| try { | |
| const response = await github.rest.dependabot.listAlertsForRepo({ | |
| owner, | |
| repo, | |
| state: 'open', | |
| per_page: 100 | |
| }); | |
| alerts = response.data; | |
| } catch (error) { | |
| if (error.status === 403 || error.status === 404) { | |
| console.log('ERROR: Unable to access Dependabot alerts. This is likely because:'); | |
| console.log('1. Dependabot alerts are not enabled for this repository, OR'); | |
| console.log('2. The GITHUB_TOKEN does not have the required `vulnerability-alerts: read` permission'); | |
| console.log(''); | |
| console.log('To fix: Enable Dependabot alerts in repo settings or ensure the workflow has appropriate permissions'); | |
| fs.writeFileSync(alertsFile, '[]'); | |
| core.setOutput('alerts_count', '0'); | |
| return; | |
| } | |
| throw error; | |
| } | |
| if (alerts.length === 0) { | |
| console.log('No open Dependabot alerts found.'); | |
| fs.writeFileSync(alertsFile, '[]'); | |
| core.setOutput('alerts_count', '0'); | |
| return; | |
| } | |
| console.log(`Found ${alerts.length} open Dependabot alerts.`); | |
| fs.writeFileSync(alertsFile, JSON.stringify(alerts)); | |
| core.setOutput('alerts_count', String(alerts.length)); | |
| - name: Create PRs and send Slack notifications | |
| id: create-prs | |
| if: steps.fetch-alerts.outputs.alerts_count != '0' | |
| uses: actions/github-script@v8 | |
| env: | |
| # ============================================================ | |
| # DEVIN PROMPT CONFIGURATION | |
| # Edit the prompt below to customize instructions for Devin | |
| # ============================================================ | |
| DEVIN_PROMPT: | | |
| @devin-ai-integration Please resolve this Dependabot security alert. | |
| **Instructions:** | |
| 1. Analyze the vulnerability and understand its impact | |
| 2. Update the affected dependency to a secure version | |
| 3. Ideally resolve this without using an override - prefer updating the dependency directly | |
| 4. If an override is absolutely necessary, document why in the PR description | |
| 5. Run tests to ensure the update doesn't break anything | |
| 6. Push your fix to this PR branch and tag @davidkonigsberg for review | |
| 7. Delete the scaffold file (.github/dependabot-alerts/alert-*.md) as part of your fix | |
| **Alert Details:** | |
| ALERTS_FILE: ${{ runner.temp }}/dependabot-alerts.json | |
| SLACK_TOKEN: ${{ secrets.DEVIN_AI_PR_BOT_SLACK_TOKEN }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const devinPrompt = process.env.DEVIN_PROMPT; | |
| const slackToken = process.env.SLACK_TOKEN; | |
| const slackChannelId = 'C0A23CZEFNF'; | |
| const devinMention = '<@U088PL5FS3B>'; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const baseBranch = 'main'; | |
| // Sanitize text from upstream advisories so that @-mentions embedded | |
| // in third-party descriptions do not auto-tag unrelated GitHub users. | |
| const ALLOWED_MENTIONS = new Set(['devin-ai-integration', 'davidkonigsberg']); | |
| const sanitizeMentions = (text) => { | |
| if (text == null) return text; | |
| return String(text).replace( | |
| /(^|[^A-Za-z0-9`])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,38})?)/g, | |
| (match, pre, handle) => | |
| ALLOWED_MENTIONS.has(handle.toLowerCase()) | |
| ? match | |
| : `${pre}\`@${handle}\`` | |
| ); | |
| }; | |
| const alerts = JSON.parse(fs.readFileSync(process.env.ALERTS_FILE, 'utf8')); | |
| if (alerts.length === 0) { | |
| console.log('No alerts to process.'); | |
| return; | |
| } | |
| console.log(`Processing ${alerts.length} Dependabot alerts.`); | |
| // Fetch existing OPEN PRs to check for duplicates | |
| const existingPRs = await github.paginate(github.rest.pulls.list, { | |
| owner, | |
| repo, | |
| state: 'open', | |
| per_page: 100 | |
| }); | |
| // Create a set of alert numbers that already have open PRs | |
| const existingAlertNumbers = new Set(); | |
| const existingPackages = new Set(); | |
| for (const pr of existingPRs) { | |
| const alertMatch = pr.title.match(/\[Dependabot Alert #(\d+)\]/); | |
| if (alertMatch) { | |
| existingAlertNumbers.add(parseInt(alertMatch[1])); | |
| const packageMatch = pr.title.match(/\[Dependabot Alert #\d+\] \w+: (.+) vulnerability/); | |
| if (packageMatch) { | |
| existingPackages.add(packageMatch[1]); | |
| } | |
| } | |
| } | |
| console.log(`Found ${existingAlertNumbers.size} existing open PRs for Dependabot alerts.`); | |
| console.log(`Found ${existingPackages.size} packages with existing open PRs: ${[...existingPackages].join(', ')}`); | |
| // Track packages we create PRs for in this run to avoid duplicates | |
| const packagesWithNewPRs = new Set(); | |
| // Get the base branch ref for creating new branches | |
| const baseRef = await github.rest.git.getRef({ | |
| owner, | |
| repo, | |
| ref: `heads/${baseBranch}`, | |
| }); | |
| const baseCommitSha = baseRef.data.object.sha; | |
| const baseCommit = await github.rest.git.getCommit({ | |
| owner, | |
| repo, | |
| commit_sha: baseCommitSha, | |
| }); | |
| const baseTreeSha = baseCommit.data.tree.sha; | |
| // Helper function to send Slack notification | |
| async function sendSlackNotification(prInfo) { | |
| const message = `${devinMention} *New Dependabot Alert PR Created*\nNew PR created for Dependabot security alert. Please update the PR in the \`sync-openapi\` repo to address the dependabot alert "<${prInfo.alertUrl}|${prInfo.alertName}>". Follow the instructions already in the <${prInfo.url}|PR>.`; | |
| try { | |
| const response = await fetch('https://slack.com/api/chat.postMessage', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${slackToken}` | |
| }, | |
| body: JSON.stringify({ | |
| channel: slackChannelId, | |
| text: message, | |
| mrkdwn: true | |
| }) | |
| }); | |
| const result = await response.json(); | |
| if (result.ok) { | |
| console.log(`Sent Slack notification for PR #${prInfo.number}: ${prInfo.alertName}`); | |
| } else { | |
| console.error(`Failed to send Slack notification for PR #${prInfo.number}: ${result.error}`); | |
| } | |
| } catch (error) { | |
| console.error(`Error sending Slack notification for PR #${prInfo.number}: ${error.message}`); | |
| } | |
| } | |
| // Create PRs for new alerts | |
| let createdCount = 0; | |
| let skippedDueToPackage = 0; | |
| for (const alert of alerts) { | |
| if (existingAlertNumbers.has(alert.number)) { | |
| console.log(`PR already exists for alert #${alert.number}, skipping.`); | |
| continue; | |
| } | |
| const severity = alert.security_advisory?.severity || 'unknown'; | |
| const packageName = alert.security_vulnerability?.package?.name || 'unknown package'; | |
| // Skip if there's already an open PR for this package | |
| if (existingPackages.has(packageName) || packagesWithNewPRs.has(packageName)) { | |
| console.log(`PR already exists for package "${packageName}" (alert #${alert.number}), skipping to avoid duplicate PRs.`); | |
| skippedDueToPackage++; | |
| continue; | |
| } | |
| const ecosystem = alert.security_vulnerability?.package?.ecosystem || 'unknown'; | |
| const vulnerableVersionRange = alert.security_vulnerability?.vulnerable_version_range || 'unknown'; | |
| const patchedVersions = alert.security_vulnerability?.first_patched_version?.identifier || 'No patch available'; | |
| const cveId = alert.security_advisory?.cve_id || 'N/A'; | |
| const ghsaId = alert.security_advisory?.ghsa_id || 'N/A'; | |
| const summary = sanitizeMentions(alert.security_advisory?.summary) || 'No summary available'; | |
| const description = sanitizeMentions(alert.security_advisory?.description) || 'No description available'; | |
| const manifestPath = alert.dependency?.manifest_path || 'unknown'; | |
| const branchName = `dependabot-alert-${alert.number}-devin`; | |
| const filePath = `.github/dependabot-alerts/alert-${alert.number}.md`; | |
| // Check if branch already exists | |
| let branchExists = false; | |
| try { | |
| await github.rest.git.getRef({ | |
| owner, | |
| repo, | |
| ref: `heads/${branchName}`, | |
| }); | |
| branchExists = true; | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| if (branchExists) { | |
| const prForBranch = existingPRs.find(pr => pr.head.ref === branchName); | |
| if (prForBranch) { | |
| console.log(`Skipping alert #${alert.number}: branch ${branchName} has an open PR #${prForBranch.number}`); | |
| continue; | |
| } | |
| // No open PR for this branch, delete it so we can create a fresh one | |
| console.log(`Deleting old branch ${branchName} from closed PR...`); | |
| await github.rest.git.deleteRef({ | |
| owner, | |
| repo, | |
| ref: `heads/${branchName}`, | |
| }); | |
| } | |
| const prTitle = `[Dependabot Alert #${alert.number}] ${severity.toUpperCase()}: ${packageName} vulnerability`; | |
| const alertContent = `${devinPrompt} | |
| - **Package:** ${packageName} (${ecosystem}) | |
| - **Severity:** ${severity.toUpperCase()} | |
| - **Vulnerable versions:** ${vulnerableVersionRange} | |
| - **Patched version:** ${patchedVersions} | |
| - **CVE:** ${cveId} | |
| - **GHSA:** ${ghsaId} | |
| - **Manifest:** ${manifestPath} | |
| **Summary:** | |
| ${summary} | |
| **Description:** | |
| ${description} | |
| --- | |
| [View Dependabot Alert](https://github.com/${owner}/${repo}/security/dependabot/${alert.number}) | |
| `; | |
| try { | |
| // Create a blob with the alert content | |
| const blob = await github.rest.git.createBlob({ | |
| owner, | |
| repo, | |
| content: alertContent, | |
| encoding: 'utf-8', | |
| }); | |
| // Create a tree with the new file | |
| const tree = await github.rest.git.createTree({ | |
| owner, | |
| repo, | |
| base_tree: baseTreeSha, | |
| tree: [ | |
| { | |
| path: filePath, | |
| mode: '100644', | |
| type: 'blob', | |
| sha: blob.data.sha, | |
| }, | |
| ], | |
| }); | |
| // Create a commit | |
| const commit = await github.rest.git.createCommit({ | |
| owner, | |
| repo, | |
| message: `[Dependabot Alert #${alert.number}] Scaffold PR for ${packageName}`, | |
| tree: tree.data.sha, | |
| parents: [baseCommitSha], | |
| }); | |
| // Create the branch | |
| await github.rest.git.createRef({ | |
| owner, | |
| repo, | |
| ref: `refs/heads/${branchName}`, | |
| sha: commit.data.sha, | |
| }); | |
| // Create the PR | |
| const pr = await github.rest.pulls.create({ | |
| owner, | |
| repo, | |
| title: prTitle, | |
| head: branchName, | |
| base: baseBranch, | |
| body: alertContent, | |
| draft: true, | |
| }); | |
| console.log(`Created PR for alert #${alert.number}: ${packageName}`); | |
| createdCount++; | |
| // Track this package so we don't create duplicate PRs | |
| packagesWithNewPRs.add(packageName); | |
| // Send Slack notification | |
| const alertUrl = `https://github.com/${owner}/${repo}/security/dependabot/${alert.number}`; | |
| await sendSlackNotification({ | |
| number: pr.data.number, | |
| url: pr.data.html_url, | |
| alertName: summary, | |
| alertUrl | |
| }); | |
| } catch (error) { | |
| console.error(`Failed to create PR for alert #${alert.number}: ${error.message}`); | |
| } | |
| } | |
| console.log(`Created ${createdCount} new PRs from Dependabot alerts.`); | |
| if (skippedDueToPackage > 0) { | |
| console.log(`Skipped ${skippedDueToPackage} alerts because PRs already exist for those packages.`); | |
| } |