Files
prowler/.github/scripts/grype-pr-comment.js
T

101 lines
4.2 KiB
JavaScript

const fs = require('fs');
// Configuration from environment variables
const REPORT_FILE = process.env.GRYPE_REPORT_FILE || 'grype-report.json';
const IMAGE_NAME = process.env.IMAGE_NAME || 'container-image';
const GITHUB_SHA = process.env.GITHUB_SHA || 'unknown';
const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY || '';
const GITHUB_RUN_ID = process.env.GITHUB_RUN_ID || '';
const CUTOFF = process.env.CUTOFF || 'high';
// A cutoff of 'critical' blocks only on critical; anything else blocks on high and above
const blocking = CUTOFF === 'critical' ? ['Critical'] : ['Critical', 'High'];
const report = JSON.parse(fs.readFileSync(REPORT_FILE, 'utf-8'));
const matches = Array.isArray(report.matches) ? report.matches : [];
const ignored = Array.isArray(report.ignoredMatches) ? report.ignoredMatches : [];
const counts = { Critical: 0, High: 0, Medium: 0, Low: 0, Negligible: 0, Unknown: 0 };
const blockers = new Map();
for (const match of matches) {
const severity = match.vulnerability.severity;
if (counts[severity] !== undefined) {
counts[severity]++;
}
if (blocking.includes(severity)) {
const artifact = match.artifact;
const fixedIn = (match.vulnerability.fix && match.vulnerability.fix.versions || []).join(', ');
// Same CVE can match several install paths of one package; collapse them
blockers.set(`${match.vulnerability.id}|${artifact.name}`, {
id: match.vulnerability.id,
severity,
name: artifact.name,
version: artifact.version,
fixedIn
});
}
}
const ignoredBlocking = ignored.filter(m => blocking.includes(m.vulnerability.severity)).length;
const shortSha = GITHUB_SHA.substring(0, 7);
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
const severityConfig = {
Critical: { icon: '🔴', label: 'Critical' },
High: { icon: '🟠', label: 'High' },
Medium: { icon: '🟡', label: 'Medium' },
Low: { icon: '🔵', label: 'Low' }
};
let comment = '## 🔎 Container Security Scan (Grype)\n\n';
comment += `**Image:** \`${IMAGE_NAME}:${shortSha}\`\n`;
comment += `**Last scan:** ${timestamp}\n\n`;
if (blockers.size === 0) {
comment += '### ✅ Nothing Blocking\n\n';
comment += `No findings at **${blocking.join(' or ').toLowerCase()}** severity.\n`;
} else {
comment += `### ⚠️ ${blockers.size} Finding(s) Blocking This PR\n\n`;
comment += '| Severity | CVE | Package | Installed | Fixed in |\n';
comment += '|---|---|---|---|---|\n';
const order = { Critical: 0, High: 1 };
const rows = [...blockers.values()].sort((a, b) =>
(order[a.severity] - order[b.severity]) || a.name.localeCompare(b.name));
for (const row of rows) {
const config = severityConfig[row.severity];
comment += `| ${config.icon} ${config.label} | \`${row.id}\` | \`${row.name}\` | ${row.version} | ${row.fixedIn || '—'} |\n`;
}
comment += '\n**What to do:**\n';
comment += '- Upgrade the package to the version in the "Fixed in" column.\n';
comment += '- If it is pinned by another dependency, or the fix is otherwise out of reach, add it to `.grype.yaml` **with the reason**.\n';
comment += '- Findings with no published fix never appear here: the scan runs with `only-fixed`, so it reports only what can actually be acted on.\n';
}
const otherCounts = Object.entries(counts)
.filter(([severity, count]) => !blocking.includes(severity) && count > 0)
.map(([severity, count]) => `${severity.toLowerCase()}: ${count}`);
if (otherCounts.length > 0) {
comment += `\nNot blocking at this cutoff — ${otherCounts.join(', ')}.\n`;
}
if (ignoredBlocking > 0) {
comment += `\n${ignoredBlocking} finding(s) excluded by \`.grype.yaml\`, each with a documented reason.\n`;
}
comment += '\n---\n';
comment += '📋 **Resources:**\n';
if (GITHUB_REPOSITORY && GITHUB_RUN_ID) {
comment += `- [Download full report](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) (see artifacts)\n`;
}
comment += '- [View in Security tab](https://github.com/' + (GITHUB_REPOSITORY || 'repository') + '/security/code-scanning)\n';
comment += '- Scanned with [Grype](https://github.com/anchore/grype), alongside Trivy\n';
module.exports = comment;