Skip to content
Merged
Show file tree
Hide file tree
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
146 changes: 0 additions & 146 deletions .github/workflows/check-code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,152 +103,6 @@ jobs:
- name: Verify platform-integrations matches a fresh render of plugin-source
run: uv run python plugin-source/build_plugins.py check

check-vulnerabilities:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
python-version: '3.12'
- name: Export requirements from lockfile
run: uv export --no-hashes --frozen > /tmp/requirements.txt
- name: Install pip-audit
run: uv tool install pip-audit
- name: Run pip-audit
id: audit
continue-on-error: true
run: pip-audit -r /tmp/requirements.txt --format json --output /tmp/audit-results.json
- name: Process results and gate on critical CVEs
if: steps.audit.outcome == 'failure'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = JSON.parse(fs.readFileSync('/tmp/audit-results.json', 'utf8'));

const vulns = [];
for (const dep of results.dependencies || []) {
for (const vuln of dep.vulns || []) {
vulns.push({ name: dep.name, version: dep.version, id: vuln.id, fix_versions: vuln.fix_versions || [] });
}
}

if (vulns.length === 0) return;

// Query OSV API for severity of each unique vuln ID
const uniqueIds = [...new Set(vulns.map(v => v.id))];
const severityMap = {};

for (const id of uniqueIds) {
try {
const resp = await fetch(`https://api.osv.dev/v1/vulns/${id}`);
if (!resp.ok) continue;
const data = await resp.json();
// Extract CVSS score from severity or database_specific
let score = 0;
if (data.severity && data.severity.length > 0) {
for (const s of data.severity) {
if (s.type === 'CVSS_V3' || s.type === 'CVSS_V4') {
// Parse base score from vector — last metric group
const match = s.score?.match(/CVSS:\d\.\d\/(.+)/);
if (match) {
// Use database_specific for numeric score if available
}
}
}
}
// Check database_specific for numeric CVSS
if (data.database_specific?.cvss_v3) {
score = typeof data.database_specific.cvss_v3 === 'number'
? data.database_specific.cvss_v3
: parseFloat(data.database_specific.cvss_v3) || 0;
}
if (data.database_specific?.severity) {
const sev = data.database_specific.severity.toUpperCase();
if (sev === 'CRITICAL') score = Math.max(score, 9.0);
else if (sev === 'HIGH') score = Math.max(score, 7.0);
}
// Fallback: check affected[].ecosystem_specific or aliases for NVD
if (score === 0 && data.affected) {
for (const a of data.affected) {
if (a.database_specific?.cvss_v3) {
score = Math.max(score, parseFloat(a.database_specific.cvss_v3) || 0);
}
}
}
severityMap[id] = score;
} catch (e) {
console.log(`Warning: could not fetch severity for ${id}`);
severityMap[id] = 0;
}
}

// Categorize
const critical = vulns.filter(v => (severityMap[v.id] || 0) >= 9.0);
const high = vulns.filter(v => {
const s = severityMap[v.id] || 0;
return s >= 7.0 && s < 9.0;
});

console.log(`Found ${critical.length} critical, ${high.length} high vulnerabilities`);

// Create/update GH issue for high+critical
if (critical.length > 0 || high.length > 0) {
const lines = ['## Automated CVE Scan Results\n', `_Last scanned: ${new Date().toISOString().split('T')[0]}_\n`];
if (critical.length) {
lines.push('### Critical (CVSS >= 9.0)\n');
critical.forEach(v => lines.push(`- **${v.id}** in \`${v.name}==${v.version}\` — fix: ${v.fix_versions.join(', ') || 'none available'}`));
}
if (high.length) {
lines.push('\n### High (CVSS >= 7.0)\n');
high.forEach(v => lines.push(`- **${v.id}** in \`${v.name}==${v.version}\` — fix: ${v.fix_versions.join(', ') || 'none available'}`));
}

const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'security,automated',
state: 'open'
});

const title = `[Security] ${critical.length} critical, ${high.length} high vulnerabilities detected`;
const existingIssue = existing.data.find(i => i.labels.some(l => l.name === 'automated'));

if (existingIssue) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
title,
body: lines.join('\n')
});
console.log(`Updated issue #${existingIssue.number}`);
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body: lines.join('\n'),
labels: ['security', 'automated']
});
console.log('Created new security issue');
}
}

// Report but never block the build
if (critical.length > 0) {
core.warning(`${critical.length} critical CVEs found: ${critical.map(v => v.id).join(', ')} — tracked in GitHub issue`);
} else {
console.log('No critical vulnerabilities. High-severity issues tracked in GitHub issue.');
}

ui-tests:
runs-on: ubuntu-latest
steps:
Expand Down
140 changes: 140 additions & 0 deletions .github/workflows/check-vulnerabilities.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
name: Check Vulnerabilities

on:
pull_request:
schedule:
- cron: '0 8 * * 1'

permissions:
contents: read
issues: write
Comment on lines +3 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

pull_request trigger will write GitHub issues from PR context (fails on fork PRs, noisy on same-repo PRs).

The issue create/update in the Process results step runs on every pull_request event when vulns are found. For PRs from forks, GITHUB_TOKEN is downgraded to read-only regardless of the permissions: block, so issues.create/issues.update return 403 — and since this github-script step isn't continue-on-error, the job fails. For same-repo PRs it will create/update the shared security issue on every PR, duplicating the scheduled run.

Consider restricting the issue-filing logic to the scheduled event (e.g. gate the write on github.event_name == 'schedule'), or scope the PR run to reporting-only.

🔧 Example: gate issue writes to scheduled runs
             // Create/update GH issue for high+critical
-            if (critical.length > 0 || high.length > 0) {
+            if ((critical.length > 0 || high.length > 0) && context.eventName === 'schedule') {

Also applies to: 32-33

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/check-vulnerabilities.yaml around lines 3 - 10, The
workflow currently lets the vulnerability-filing logic run on pull_request
events, which can fail on forked PRs and create noisy duplicate issues on normal
PRs. Update the check-vulnerabilities workflow so the issue creation/update
logic in the Process results step only runs for scheduled executions by checking
github.event_name in that step, or otherwise make the pull_request path
reporting-only. Keep the change focused around the Process results github-script
and the existing on: trigger block.


jobs:
check-vulnerabilities:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
python-version: '3.12'
- name: Export requirements from lockfile
run: uv export --no-hashes --frozen > /tmp/requirements.txt
- name: Install pip-audit
run: uv tool install pip-audit
- name: Run pip-audit
id: audit
continue-on-error: true
run: pip-audit -r /tmp/requirements.txt --format json --output /tmp/audit-results.json
- name: Process results and report CVEs
if: steps.audit.outcome == 'failure'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = JSON.parse(fs.readFileSync('/tmp/audit-results.json', 'utf8'));
Comment on lines +37 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against a missing/empty audit-results file.

steps.audit.outcome == 'failure' fires whenever pip-audit exits non-zero, which includes tool errors — not only when vulnerabilities are found. In those cases /tmp/audit-results.json may be absent or empty, and readFileSync/JSON.parse will throw and fail the step. A small existence/parse guard makes this robust.

🛡️ Proposed guard
             const fs = require('fs');
-            const results = JSON.parse(fs.readFileSync('/tmp/audit-results.json', 'utf8'));
+            if (!fs.existsSync('/tmp/audit-results.json')) {
+              console.log('No audit results file found; pip-audit may have errored. Skipping.');
+              return;
+            }
+            let results;
+            try {
+              results = JSON.parse(fs.readFileSync('/tmp/audit-results.json', 'utf8'));
+            } catch (e) {
+              core.warning(`Could not parse audit results: ${e.message}`);
+              return;
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fs = require('fs');
const results = JSON.parse(fs.readFileSync('/tmp/audit-results.json', 'utf8'));
const fs = require('fs');
if (!fs.existsSync('/tmp/audit-results.json')) {
console.log('No audit results file found; pip-audit may have errored. Skipping.');
return;
}
let results;
try {
results = JSON.parse(fs.readFileSync('/tmp/audit-results.json', 'utf8'));
} catch (e) {
core.warning(`Could not parse audit results: ${e.message}`);
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/check-vulnerabilities.yaml around lines 37 - 38, The
audit-results handling in the workflow’s Node.js snippet should guard against
missing or empty /tmp/audit-results.json before calling readFileSync and
JSON.parse. Update the logic around the existing fs usage so it first checks the
file exists and has content, then only parses when valid, otherwise skips JSON
loading and handles the no-results case gracefully in the audit step.


const vulns = [];
for (const dep of results.dependencies || []) {
for (const vuln of dep.vulns || []) {
vulns.push({ name: dep.name, version: dep.version, id: vuln.id, fix_versions: vuln.fix_versions || [] });
}
}

if (vulns.length === 0) return;

// Query OSV API for severity of each unique vuln ID
const uniqueIds = [...new Set(vulns.map(v => v.id))];
const severityMap = {};

for (const id of uniqueIds) {
try {
const resp = await fetch(`https://api.osv.dev/v1/vulns/${id}`);
if (!resp.ok) continue;
const data = await resp.json();
let score = 0;
if (data.database_specific?.cvss_v3) {
score = typeof data.database_specific.cvss_v3 === 'number'
? data.database_specific.cvss_v3
: parseFloat(data.database_specific.cvss_v3) || 0;
}
if (data.database_specific?.severity) {
const sev = data.database_specific.severity.toUpperCase();
if (sev === 'CRITICAL') score = Math.max(score, 9.0);
else if (sev === 'HIGH') score = Math.max(score, 7.0);
}
if (score === 0 && data.affected) {
for (const a of data.affected) {
if (a.database_specific?.cvss_v3) {
score = Math.max(score, parseFloat(a.database_specific.cvss_v3) || 0);
}
}
}
severityMap[id] = score;
Comment on lines +53 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

OSV API v1 vulns response severity field schema CVSS_V3 database_specific

💡 Result:

In the OSV (Open Source Vulnerability) schema, the severity of a vulnerability is represented by the severity field, which is an array of objects [1][2]. Each object in this array contains a type and a score [1][2]. The type field specifies the quantitative scoring method used [1][2]. When using CVSS, the type should be set to CVSS_V3 (or CVSS_V2 / CVSS_V4 as appropriate) [2]. The score field is a string containing the CVSS vector string (e.g., "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H") [1]. The database_specific field is a flexible, optional JSON object [2]. It is intended for storing custom or non-standardized metadata that is specific to the database providing the record [2]. It is distinct from the standardized severity field; while some databases might optionally include severity-related metadata within database_specific, such data is not part of the core, machine-readable severity schema [1][2][3]. Summary of relevant fields: - severity: A list of objects, each with a type (e.g., CVSS_V3) and score (e.g., the vector string) [1][2]. - database_specific: A general-purpose JSON structure for database-defined metadata [2][3]. It should not be used as a substitute for the severity field if the goal is to provide standardized, interoperable vulnerability scoring [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the workflow and inspect the relevant section with line numbers.
git ls-files .github/workflows/check-vulnerabilities.yaml
sed -n '1,220p' .github/workflows/check-vulnerabilities.yaml

# Find any other OSV parsing or severity handling in the repo.
rg -n "database_specific|severity\\]|CVSS_V3|osv.dev/v1/vulns|cvss_v3" .

Repository: AgentToolkit/altk-evolve

Length of output: 5885


Severity scoring should use OSV severity[] first. database_specific.cvss_v3/severity is non-standard here, so CVSS-based vulns can fall through to 0 and be skipped. Parse the top-level severity entries (CVSS vector → base score) and keep database_specific as a fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/check-vulnerabilities.yaml around lines 53 - 76, The
severity scoring logic in the OSV fetch loop currently relies on non-standard
database_specific fields, which can leave CVSS vulnerabilities at 0. Update the
parsing in the uniqueIds processing block to read the top-level severity[]
entries first, convert any CVSS vector information to a base score, and then
fall back to database_specific.cvss_v3 or database_specific.severity if needed.
Keep the existing severityMap assignment in place, but ensure the score
calculation in this fetch path prefers OSV severity data before the fallback
fields.

} catch (e) {
console.log(`Warning: could not fetch severity for ${id}`);
severityMap[id] = 0;
}
}

// Categorize
const critical = vulns.filter(v => (severityMap[v.id] || 0) >= 9.0);
const high = vulns.filter(v => {
const s = severityMap[v.id] || 0;
return s >= 7.0 && s < 9.0;
});

console.log(`Found ${critical.length} critical, ${high.length} high vulnerabilities`);

// Create/update GH issue for high+critical
if (critical.length > 0 || high.length > 0) {
const lines = ['## Automated CVE Scan Results\n', `_Last scanned: ${new Date().toISOString().split('T')[0]}_\n`];
if (critical.length) {
lines.push('### Critical (CVSS >= 9.0)\n');
critical.forEach(v => lines.push(`- **${v.id}** in \`${v.name}==${v.version}\` — fix: ${v.fix_versions.join(', ') || 'none available'}`));
}
if (high.length) {
lines.push('\n### High (CVSS >= 7.0)\n');
high.forEach(v => lines.push(`- **${v.id}** in \`${v.name}==${v.version}\` — fix: ${v.fix_versions.join(', ') || 'none available'}`));
}

const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'security,automated',
state: 'open'
});

const title = `[Security] ${critical.length} critical, ${high.length} high vulnerabilities detected`;
const existingIssue = existing.data.find(i => i.labels.some(l => l.name === 'automated'));

if (existingIssue) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
title,
body: lines.join('\n')
});
console.log(`Updated issue #${existingIssue.number}`);
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body: lines.join('\n'),
labels: ['security', 'automated']
});
console.log('Created new security issue');
}
}

// Report but never block the build
if (critical.length > 0) {
core.warning(`${critical.length} critical CVEs found: ${critical.map(v => v.id).join(', ')} — tracked in GitHub issue`);
} else {
console.log('No critical vulnerabilities. High-severity issues tracked in GitHub issue.');
}
Loading