Skip to content

Commit de28d32

Browse files
Merge pull request #8 from Elnora-AI/chore/vulnerability-issue-workflows
chore: add vulnerability issue propagation workflows
2 parents a3cc687 + b27bbbb commit de28d32

2 files changed

Lines changed: 360 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
name: Create Issues from CodeQL Alerts
2+
3+
on:
4+
schedule:
5+
# Run daily at 8 AM UTC
6+
- cron: '0 8 * * *'
7+
workflow_dispatch: # Allow manual trigger for urgent situations
8+
9+
permissions:
10+
issues: write
11+
security-events: read
12+
13+
jobs:
14+
create-issues:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Create Issues from CodeQL Alerts
18+
uses: actions/github-script@v8
19+
with:
20+
github-token: ${{ secrets.GITHUB_TOKEN }}
21+
script: |
22+
// Fetch open CodeQL alerts via GitHub API
23+
const { data: alerts } = await github.rest.codeScanning.listAlertsForRepo({
24+
owner: context.repo.owner,
25+
repo: context.repo.repo,
26+
state: 'open',
27+
per_page: 100
28+
});
29+
30+
console.log(`Found ${alerts.length} open CodeQL alerts`);
31+
32+
if (alerts.length === 0) {
33+
console.log('No open alerts to process');
34+
return;
35+
}
36+
37+
// Get existing issues to avoid duplicates
38+
const { data: existingIssues } = await github.rest.issues.listForRepo({
39+
owner: context.repo.owner,
40+
repo: context.repo.repo,
41+
labels: 'Source: CodeQL',
42+
state: 'all',
43+
per_page: 100
44+
});
45+
46+
// Extract CodeQL alert numbers from existing issue titles
47+
// Format: [SEVERITY] CodeQL #123: rule-name - description
48+
const existingAlertNumbers = existingIssues.map(issue => {
49+
const match = issue.title.match(/CodeQL #(\d+):/);
50+
return match ? parseInt(match[1], 10) : null;
51+
}).filter(Boolean);
52+
53+
console.log(`Found ${existingAlertNumbers.length} existing CodeQL issues`);
54+
55+
let createdCount = 0;
56+
let skippedCount = 0;
57+
58+
for (const alert of alerts) {
59+
const alertNumber = alert.number;
60+
const ruleId = alert.rule?.id || 'unknown-rule';
61+
const ruleName = alert.rule?.name || ruleId;
62+
const ruleDescription = alert.rule?.description || 'No description available';
63+
const severity = (alert.rule?.security_severity_level || 'low').toUpperCase();
64+
65+
// Skip if issue already exists for this alert
66+
if (existingAlertNumbers.includes(alertNumber)) {
67+
console.log(`Skipping CodeQL #${alertNumber} - issue already exists`);
68+
skippedCount++;
69+
continue;
70+
}
71+
72+
// Determine SLA based on severity (matches company security policy)
73+
const slaMap = {
74+
'CRITICAL': '30 days',
75+
'HIGH': '30 days',
76+
'MEDIUM': '60 days',
77+
'LOW': '90 days'
78+
};
79+
const sla = slaMap[severity] || '90 days';
80+
81+
// Map severity to label format
82+
const severityLabelMap = {
83+
'CRITICAL': 'Severity: Critical',
84+
'HIGH': 'Severity: High',
85+
'MEDIUM': 'Severity: Medium',
86+
'LOW': 'Severity: Low'
87+
};
88+
const severityLabel = severityLabelMap[severity] || 'Severity: Low';
89+
90+
// Extract location information
91+
const location = alert.most_recent_instance?.location || {};
92+
const filePath = location.path || 'Unknown file';
93+
const startLine = location.start_line || 'N/A';
94+
const endLine = location.end_line || startLine;
95+
const lineRange = startLine === endLine ? `Line ${startLine}` : `Lines ${startLine}-${endLine}`;
96+
97+
// Extract message
98+
const message = alert.most_recent_instance?.message?.text || 'No message available';
99+
100+
// Extract tool information
101+
const toolName = alert.tool?.name || 'CodeQL';
102+
const toolVersion = alert.tool?.version || 'Unknown';
103+
104+
// Get alert URL
105+
const alertUrl = alert.html_url;
106+
107+
// Get rule tags for context
108+
const ruleTags = (alert.rule?.tags || []).join(', ') || 'None';
109+
110+
// Build the issue body
111+
const issueBody = `## CodeQL Alert Details
112+
113+
| Field | Value |
114+
|-------|-------|
115+
| **Alert Number** | #${alertNumber} |
116+
| **Severity** | ${severity} |
117+
| **SLA** | ${sla} |
118+
| **Rule ID** | \`${ruleId}\` |
119+
| **File** | \`${filePath}\` |
120+
| **Location** | ${lineRange} |
121+
| **Tool** | ${toolName} v${toolVersion} |
122+
123+
## Rule Description
124+
125+
${ruleDescription}
126+
127+
## Finding
128+
129+
${message}
130+
131+
## Location
132+
133+
\`\`\`
134+
${filePath}:${startLine}
135+
\`\`\`
136+
137+
## Rule Tags
138+
139+
${ruleTags}
140+
141+
## Remediation
142+
143+
Review the code at the specified location and apply the recommended fix.
144+
See the CodeQL alert for detailed remediation guidance.
145+
146+
## Links
147+
148+
- [View CodeQL Alert](${alertUrl})
149+
150+
---
151+
152+
*Auto-generated from CodeQL alert on ${new Date().toISOString().split('T')[0]}*
153+
*This issue will sync to Linear for triage and remediation tracking.*`;
154+
155+
// Create the issue
156+
const shortDescription = ruleDescription.length > 60
157+
? ruleDescription.substring(0, 60) + '...'
158+
: ruleDescription;
159+
const issueTitle = `[${severity}] CodeQL #${alertNumber}: ${ruleId} - ${shortDescription}`;
160+
161+
try {
162+
await github.rest.issues.create({
163+
owner: context.repo.owner,
164+
repo: context.repo.repo,
165+
title: issueTitle,
166+
body: issueBody,
167+
labels: ['Source: CodeQL', 'Flag: security', severityLabel]
168+
});
169+
170+
console.log(`Created issue for CodeQL #${alertNumber}: ${ruleId}`);
171+
createdCount++;
172+
173+
// Add to tracking to prevent duplicates within same run
174+
existingAlertNumbers.push(alertNumber);
175+
} catch (error) {
176+
console.error(`Failed to create issue for CodeQL #${alertNumber}: ${error.message}`);
177+
}
178+
}
179+
180+
console.log(`\nSummary: Created ${createdCount} issues, skipped ${skippedCount} (already exist)`);
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
name: Create Issues from Dependabot Alerts
2+
3+
on:
4+
schedule:
5+
# Run daily at 7 AM UTC
6+
- cron: '0 7 * * *'
7+
workflow_dispatch: # Allow manual trigger for urgent situations
8+
9+
permissions:
10+
issues: write
11+
12+
jobs:
13+
create-issues:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Create Issues from Dependabot Alerts
17+
uses: actions/github-script@v8
18+
env:
19+
DEPENDABOT_TOKEN: ${{ secrets.DEPENDABOT_PAT }}
20+
with:
21+
github-token: ${{ secrets.GITHUB_TOKEN }}
22+
script: |
23+
// Fetch Dependabot alerts using PAT via fetch API
24+
const alertsResponse = await fetch(
25+
`https://api.github.com/repos/${context.repo.owner}/${context.repo.repo}/dependabot/alerts?state=open&per_page=100`,
26+
{
27+
headers: {
28+
'Authorization': `Bearer ${process.env.DEPENDABOT_TOKEN}`,
29+
'Accept': 'application/vnd.github+json',
30+
'X-GitHub-Api-Version': '2022-11-28'
31+
}
32+
}
33+
);
34+
35+
if (!alertsResponse.ok) {
36+
throw new Error(`Failed to fetch Dependabot alerts: ${alertsResponse.status} ${alertsResponse.statusText}`);
37+
}
38+
39+
const alerts = await alertsResponse.json();
40+
console.log(`Found ${alerts.length} open Dependabot alerts`);
41+
42+
if (alerts.length === 0) {
43+
console.log('No open alerts to process');
44+
return;
45+
}
46+
47+
// Get existing issues to avoid duplicates
48+
const { data: existingIssues } = await github.rest.issues.listForRepo({
49+
owner: context.repo.owner,
50+
repo: context.repo.repo,
51+
labels: 'Source: Vulnerability Scan',
52+
state: 'all',
53+
per_page: 100
54+
});
55+
56+
// Extract CVE/GHSA identifiers from existing issue titles
57+
const existingIdentifiers = existingIssues.map(issue => {
58+
const cveMatch = issue.title.match(/CVE-\d{4}-\d+/);
59+
const ghsaMatch = issue.title.match(/GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}/);
60+
return cveMatch ? cveMatch[0] : (ghsaMatch ? ghsaMatch[0] : null);
61+
}).filter(Boolean);
62+
63+
console.log(`Found ${existingIdentifiers.length} existing vulnerability issues`);
64+
65+
let createdCount = 0;
66+
let skippedCount = 0;
67+
68+
for (const alert of alerts) {
69+
const cve = alert.security_advisory?.cve_id;
70+
const ghsa = alert.security_advisory?.ghsa_id;
71+
const identifier = cve || (ghsa ? `GHSA-${ghsa}` : `ALERT-${alert.number}`);
72+
const severity = (alert.security_vulnerability?.severity || 'unknown').toUpperCase();
73+
74+
// Skip if issue already exists for this vulnerability
75+
if (existingIdentifiers.includes(identifier) || existingIdentifiers.includes(cve) || existingIdentifiers.includes(ghsa)) {
76+
console.log(`Skipping ${identifier} - issue already exists`);
77+
skippedCount++;
78+
continue;
79+
}
80+
81+
// Determine SLA based on severity (matches company security policy)
82+
const slaMap = {
83+
'CRITICAL': '30 days',
84+
'HIGH': '30 days',
85+
'MEDIUM': '60 days',
86+
'LOW': '90 days'
87+
};
88+
const sla = slaMap[severity] || '90 days';
89+
90+
// Map severity to label format
91+
const severityLabelMap = {
92+
'CRITICAL': 'Severity: Critical',
93+
'HIGH': 'Severity: High',
94+
'MEDIUM': 'Severity: Medium',
95+
'LOW': 'Severity: Low'
96+
};
97+
const severityLabel = severityLabelMap[severity] || 'Severity: Low';
98+
99+
// Format the issue body with all vulnerability details
100+
const packageName = alert.security_vulnerability?.package?.name || 'Unknown package';
101+
const ecosystem = alert.security_vulnerability?.package?.ecosystem || 'Unknown';
102+
const vulnerableRange = alert.security_vulnerability?.vulnerable_version_range || 'Unknown';
103+
const patchedVersion = alert.security_vulnerability?.first_patched_version?.identifier || 'No patch available';
104+
const summary = alert.security_advisory?.summary || 'No summary available';
105+
const description = alert.security_advisory?.description || 'No description available';
106+
const manifestPath = alert.dependency?.manifest_path || 'Unknown';
107+
const alertUrl = alert.html_url;
108+
const cvssScore = alert.security_advisory?.cvss?.score || 'N/A';
109+
const cvssVector = alert.security_advisory?.cvss?.vector_string || 'N/A';
110+
111+
// Build references list
112+
const references = (alert.security_advisory?.references || [])
113+
.map(ref => `- ${ref.url}`)
114+
.join('\n') || 'No references available';
115+
116+
const issueBody = `## Vulnerability Details
117+
118+
| Field | Value |
119+
|-------|-------|
120+
| **Identifier** | ${identifier} |
121+
| **Severity** | ${severity} |
122+
| **CVSS Score** | ${cvssScore} |
123+
| **SLA** | ${sla} |
124+
| **Package** | ${packageName} (${ecosystem}) |
125+
| **Vulnerable Version** | ${vulnerableRange} |
126+
| **Patched Version** | ${patchedVersion} |
127+
| **Manifest File** | \`${manifestPath}\` |
128+
129+
## Summary
130+
131+
${summary}
132+
133+
## Description
134+
135+
${description}
136+
137+
## CVSS Vector
138+
139+
\`${cvssVector}\`
140+
141+
## References
142+
143+
${references}
144+
145+
## Remediation
146+
147+
Update \`${packageName}\` to version \`${patchedVersion}\` or later.
148+
149+
## Links
150+
151+
- [View Dependabot Alert](${alertUrl})
152+
153+
---
154+
155+
*Auto-generated from Dependabot alert on ${new Date().toISOString().split('T')[0]}*
156+
*This issue will sync to Linear for triage and remediation tracking.*`;
157+
158+
// Create the issue
159+
const issueTitle = `[${severity}] ${identifier}: ${summary.substring(0, 80)}`;
160+
161+
try {
162+
await github.rest.issues.create({
163+
owner: context.repo.owner,
164+
repo: context.repo.repo,
165+
title: issueTitle,
166+
body: issueBody,
167+
labels: ['Source: Vulnerability Scan', 'Flag: security', severityLabel]
168+
});
169+
170+
console.log(`Created issue for ${identifier}: ${packageName}`);
171+
createdCount++;
172+
173+
// Add to tracking to prevent duplicates within same run
174+
existingIdentifiers.push(identifier);
175+
} catch (error) {
176+
console.error(`Failed to create issue for ${identifier}: ${error.message}`);
177+
}
178+
}
179+
180+
console.log(`\nSummary: Created ${createdCount} issues, skipped ${skippedCount} (already exist)`);

0 commit comments

Comments
 (0)