@@ -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 :
0 commit comments