Skip to content

Commit c91bdd0

Browse files
gaodan-fangclaude
andauthored
fix(ci): move CVE scan to separate workflow to fix permission conflict (#282)
The reusable workflow (check-code.yaml) called by release-github.yaml cannot have jobs requesting issues:write. Moved check-vulnerabilities to its own workflow with proper permissions and a weekly schedule. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2cae2fc commit c91bdd0

2 files changed

Lines changed: 140 additions & 146 deletions

File tree

.github/workflows/check-code.yaml

Lines changed: 0 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -103,152 +103,6 @@ jobs:
103103
- name: Verify platform-integrations matches a fresh render of plugin-source
104104
run: uv run python plugin-source/build_plugins.py check
105105

106-
check-vulnerabilities:
107-
runs-on: ubuntu-latest
108-
permissions:
109-
contents: read
110-
issues: write
111-
steps:
112-
- uses: actions/checkout@v5
113-
with:
114-
persist-credentials: false
115-
- name: Install uv
116-
uses: astral-sh/setup-uv@v6
117-
with:
118-
enable-cache: true
119-
python-version: '3.12'
120-
- name: Export requirements from lockfile
121-
run: uv export --no-hashes --frozen > /tmp/requirements.txt
122-
- name: Install pip-audit
123-
run: uv tool install pip-audit
124-
- name: Run pip-audit
125-
id: audit
126-
continue-on-error: true
127-
run: pip-audit -r /tmp/requirements.txt --format json --output /tmp/audit-results.json
128-
- name: Process results and gate on critical CVEs
129-
if: steps.audit.outcome == 'failure'
130-
uses: actions/github-script@v7
131-
with:
132-
script: |
133-
const fs = require('fs');
134-
const results = JSON.parse(fs.readFileSync('/tmp/audit-results.json', 'utf8'));
135-
136-
const vulns = [];
137-
for (const dep of results.dependencies || []) {
138-
for (const vuln of dep.vulns || []) {
139-
vulns.push({ name: dep.name, version: dep.version, id: vuln.id, fix_versions: vuln.fix_versions || [] });
140-
}
141-
}
142-
143-
if (vulns.length === 0) return;
144-
145-
// Query OSV API for severity of each unique vuln ID
146-
const uniqueIds = [...new Set(vulns.map(v => v.id))];
147-
const severityMap = {};
148-
149-
for (const id of uniqueIds) {
150-
try {
151-
const resp = await fetch(`https://api.osv.dev/v1/vulns/${id}`);
152-
if (!resp.ok) continue;
153-
const data = await resp.json();
154-
// Extract CVSS score from severity or database_specific
155-
let score = 0;
156-
if (data.severity && data.severity.length > 0) {
157-
for (const s of data.severity) {
158-
if (s.type === 'CVSS_V3' || s.type === 'CVSS_V4') {
159-
// Parse base score from vector — last metric group
160-
const match = s.score?.match(/CVSS:\d\.\d\/(.+)/);
161-
if (match) {
162-
// Use database_specific for numeric score if available
163-
}
164-
}
165-
}
166-
}
167-
// Check database_specific for numeric CVSS
168-
if (data.database_specific?.cvss_v3) {
169-
score = typeof data.database_specific.cvss_v3 === 'number'
170-
? data.database_specific.cvss_v3
171-
: parseFloat(data.database_specific.cvss_v3) || 0;
172-
}
173-
if (data.database_specific?.severity) {
174-
const sev = data.database_specific.severity.toUpperCase();
175-
if (sev === 'CRITICAL') score = Math.max(score, 9.0);
176-
else if (sev === 'HIGH') score = Math.max(score, 7.0);
177-
}
178-
// Fallback: check affected[].ecosystem_specific or aliases for NVD
179-
if (score === 0 && data.affected) {
180-
for (const a of data.affected) {
181-
if (a.database_specific?.cvss_v3) {
182-
score = Math.max(score, parseFloat(a.database_specific.cvss_v3) || 0);
183-
}
184-
}
185-
}
186-
severityMap[id] = score;
187-
} catch (e) {
188-
console.log(`Warning: could not fetch severity for ${id}`);
189-
severityMap[id] = 0;
190-
}
191-
}
192-
193-
// Categorize
194-
const critical = vulns.filter(v => (severityMap[v.id] || 0) >= 9.0);
195-
const high = vulns.filter(v => {
196-
const s = severityMap[v.id] || 0;
197-
return s >= 7.0 && s < 9.0;
198-
});
199-
200-
console.log(`Found ${critical.length} critical, ${high.length} high vulnerabilities`);
201-
202-
// Create/update GH issue for high+critical
203-
if (critical.length > 0 || high.length > 0) {
204-
const lines = ['## Automated CVE Scan Results\n', `_Last scanned: ${new Date().toISOString().split('T')[0]}_\n`];
205-
if (critical.length) {
206-
lines.push('### Critical (CVSS >= 9.0)\n');
207-
critical.forEach(v => lines.push(`- **${v.id}** in \`${v.name}==${v.version}\` — fix: ${v.fix_versions.join(', ') || 'none available'}`));
208-
}
209-
if (high.length) {
210-
lines.push('\n### High (CVSS >= 7.0)\n');
211-
high.forEach(v => lines.push(`- **${v.id}** in \`${v.name}==${v.version}\` — fix: ${v.fix_versions.join(', ') || 'none available'}`));
212-
}
213-
214-
const existing = await github.rest.issues.listForRepo({
215-
owner: context.repo.owner,
216-
repo: context.repo.repo,
217-
labels: 'security,automated',
218-
state: 'open'
219-
});
220-
221-
const title = `[Security] ${critical.length} critical, ${high.length} high vulnerabilities detected`;
222-
const existingIssue = existing.data.find(i => i.labels.some(l => l.name === 'automated'));
223-
224-
if (existingIssue) {
225-
await github.rest.issues.update({
226-
owner: context.repo.owner,
227-
repo: context.repo.repo,
228-
issue_number: existingIssue.number,
229-
title,
230-
body: lines.join('\n')
231-
});
232-
console.log(`Updated issue #${existingIssue.number}`);
233-
} else {
234-
await github.rest.issues.create({
235-
owner: context.repo.owner,
236-
repo: context.repo.repo,
237-
title,
238-
body: lines.join('\n'),
239-
labels: ['security', 'automated']
240-
});
241-
console.log('Created new security issue');
242-
}
243-
}
244-
245-
// Report but never block the build
246-
if (critical.length > 0) {
247-
core.warning(`${critical.length} critical CVEs found: ${critical.map(v => v.id).join(', ')} — tracked in GitHub issue`);
248-
} else {
249-
console.log('No critical vulnerabilities. High-severity issues tracked in GitHub issue.');
250-
}
251-
252106
ui-tests:
253107
runs-on: ubuntu-latest
254108
steps:
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
name: Check Vulnerabilities
2+
3+
on:
4+
pull_request:
5+
schedule:
6+
- cron: '0 8 * * 1'
7+
8+
permissions:
9+
contents: read
10+
issues: write
11+
12+
jobs:
13+
check-vulnerabilities:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v5
17+
with:
18+
persist-credentials: false
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v6
21+
with:
22+
enable-cache: true
23+
python-version: '3.12'
24+
- name: Export requirements from lockfile
25+
run: uv export --no-hashes --frozen > /tmp/requirements.txt
26+
- name: Install pip-audit
27+
run: uv tool install pip-audit
28+
- name: Run pip-audit
29+
id: audit
30+
continue-on-error: true
31+
run: pip-audit -r /tmp/requirements.txt --format json --output /tmp/audit-results.json
32+
- name: Process results and report CVEs
33+
if: steps.audit.outcome == 'failure'
34+
uses: actions/github-script@v7
35+
with:
36+
script: |
37+
const fs = require('fs');
38+
const results = JSON.parse(fs.readFileSync('/tmp/audit-results.json', 'utf8'));
39+
40+
const vulns = [];
41+
for (const dep of results.dependencies || []) {
42+
for (const vuln of dep.vulns || []) {
43+
vulns.push({ name: dep.name, version: dep.version, id: vuln.id, fix_versions: vuln.fix_versions || [] });
44+
}
45+
}
46+
47+
if (vulns.length === 0) return;
48+
49+
// Query OSV API for severity of each unique vuln ID
50+
const uniqueIds = [...new Set(vulns.map(v => v.id))];
51+
const severityMap = {};
52+
53+
for (const id of uniqueIds) {
54+
try {
55+
const resp = await fetch(`https://api.osv.dev/v1/vulns/${id}`);
56+
if (!resp.ok) continue;
57+
const data = await resp.json();
58+
let score = 0;
59+
if (data.database_specific?.cvss_v3) {
60+
score = typeof data.database_specific.cvss_v3 === 'number'
61+
? data.database_specific.cvss_v3
62+
: parseFloat(data.database_specific.cvss_v3) || 0;
63+
}
64+
if (data.database_specific?.severity) {
65+
const sev = data.database_specific.severity.toUpperCase();
66+
if (sev === 'CRITICAL') score = Math.max(score, 9.0);
67+
else if (sev === 'HIGH') score = Math.max(score, 7.0);
68+
}
69+
if (score === 0 && data.affected) {
70+
for (const a of data.affected) {
71+
if (a.database_specific?.cvss_v3) {
72+
score = Math.max(score, parseFloat(a.database_specific.cvss_v3) || 0);
73+
}
74+
}
75+
}
76+
severityMap[id] = score;
77+
} catch (e) {
78+
console.log(`Warning: could not fetch severity for ${id}`);
79+
severityMap[id] = 0;
80+
}
81+
}
82+
83+
// Categorize
84+
const critical = vulns.filter(v => (severityMap[v.id] || 0) >= 9.0);
85+
const high = vulns.filter(v => {
86+
const s = severityMap[v.id] || 0;
87+
return s >= 7.0 && s < 9.0;
88+
});
89+
90+
console.log(`Found ${critical.length} critical, ${high.length} high vulnerabilities`);
91+
92+
// Create/update GH issue for high+critical
93+
if (critical.length > 0 || high.length > 0) {
94+
const lines = ['## Automated CVE Scan Results\n', `_Last scanned: ${new Date().toISOString().split('T')[0]}_\n`];
95+
if (critical.length) {
96+
lines.push('### Critical (CVSS >= 9.0)\n');
97+
critical.forEach(v => lines.push(`- **${v.id}** in \`${v.name}==${v.version}\` — fix: ${v.fix_versions.join(', ') || 'none available'}`));
98+
}
99+
if (high.length) {
100+
lines.push('\n### High (CVSS >= 7.0)\n');
101+
high.forEach(v => lines.push(`- **${v.id}** in \`${v.name}==${v.version}\` — fix: ${v.fix_versions.join(', ') || 'none available'}`));
102+
}
103+
104+
const existing = await github.rest.issues.listForRepo({
105+
owner: context.repo.owner,
106+
repo: context.repo.repo,
107+
labels: 'security,automated',
108+
state: 'open'
109+
});
110+
111+
const title = `[Security] ${critical.length} critical, ${high.length} high vulnerabilities detected`;
112+
const existingIssue = existing.data.find(i => i.labels.some(l => l.name === 'automated'));
113+
114+
if (existingIssue) {
115+
await github.rest.issues.update({
116+
owner: context.repo.owner,
117+
repo: context.repo.repo,
118+
issue_number: existingIssue.number,
119+
title,
120+
body: lines.join('\n')
121+
});
122+
console.log(`Updated issue #${existingIssue.number}`);
123+
} else {
124+
await github.rest.issues.create({
125+
owner: context.repo.owner,
126+
repo: context.repo.repo,
127+
title,
128+
body: lines.join('\n'),
129+
labels: ['security', 'automated']
130+
});
131+
console.log('Created new security issue');
132+
}
133+
}
134+
135+
// Report but never block the build
136+
if (critical.length > 0) {
137+
core.warning(`${critical.length} critical CVEs found: ${critical.map(v => v.id).join(', ')} — tracked in GitHub issue`);
138+
} else {
139+
console.log('No critical vulnerabilities. High-severity issues tracked in GitHub issue.');
140+
}

0 commit comments

Comments
 (0)