Skip to content
Merged
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
359 changes: 359 additions & 0 deletions .github/workflows/security-scanning-and-remediation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
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.`);
}