UNPKG

ipsambeatae

Version:

Shared dependencies of Compass, the MongoDB extension for VSCode and MongoSH

229 lines (195 loc) 6.08 kB
/* eslint-disable no-console */ import { promises as fs } from 'fs'; import _ from 'lodash'; import { loadDependencyFiles } from '../load-dependency-files'; import type { KnownSeverity, SnykPolicyRules, SnykTestProjectResult, VulnerabilityInfo, } from '../vulnerability'; import { hasKnownRemediation, hasExpiredPolicy, hasIgnorePolicy, isIgnored, loadSnykPolicyRules, vulnerabilityFromSnyk, severityToScore, } from '../vulnerability'; import { Command } from 'commander'; import { createVulnerabilityTickets } from '../jira'; export async function loadReports( files: string[] ): Promise<SnykTestProjectResult[]> { // turns single project result to a multi-project one, to simplify further // processing return ( await Promise.all( files.map(async (fileName) => JSON.parse(await fs.readFile(fileName, 'utf-8')) ) ) ).flat(); } type Dependency = { name: string; version: string; }; function filterApplicableVulnerabilities( snykTestResults: SnykTestProjectResult[], dependencies: Dependency[], rules: SnykPolicyRules ): VulnerabilityInfo[] { const uniqueVulnerabilities = new Map<string, VulnerabilityInfo>(); snykTestResults.forEach((projectResult) => { projectResult.vulnerabilities.forEach((snykVulnerability) => { if (snykVulnerability.type === 'license') { return; } const newVulnerability: VulnerabilityInfo = vulnerabilityFromSnyk( snykVulnerability, rules ); for (const dep of dependencies) { if ( newVulnerability.packageName !== dep.name || newVulnerability.packageVersion !== dep.version ) { continue; } const key = `${newVulnerability.packageName}@${newVulnerability.packageVersion}_${newVulnerability.id}`; const previouslyAdded = uniqueVulnerabilities.get(key); if (previouslyAdded) { // if already included we append the new vulnerable path previouslyAdded.origins = Array.from( new Set([...previouslyAdded.origins, ...newVulnerability.origins]) ); } else { uniqueVulnerabilities.set(key, newVulnerability); } } }); }); const sortedVulnerabilities = Array.from(uniqueVulnerabilities.values()).sort( (a, b) => `${a.packageName}@${a.packageVersion}`.localeCompare( `${b.packageName}@${b.packageVersion}` ) ); return sortedVulnerabilities; } function fail( failOn: KnownSeverity, bundleVulnerabilities: VulnerabilityInfo[] ) { const minScore = severityToScore(failOn) ?? 0; for (const vuln of bundleVulnerabilities) { if ( (vuln.score === undefined || vuln.score >= minScore) && // >= severity, always fail if unknown hasKnownRemediation(vuln) && !isIgnored(vuln) ) { throw new Error( `Vulnerabilities check failed: found vulnerabilities >= "${failOn}"` ); } } } export function formatIgnored(vuln: VulnerabilityInfo): string { if (!hasKnownRemediation(vuln)) { return 'Reason: Remediation not available yet'; } if (hasIgnorePolicy(vuln)) { const expired = hasExpiredPolicy(vuln) ? ' (Expired)' : ''; return `Reason: ${vuln.policy?.reason ?? 'unknown'}${expired}`; } return '-'; } export function generateVulnerabilityTable( vulnerabilities: VulnerabilityInfo[] ): string { let output = ''; output += '| dep@version | id | score | fixed in | ignored |\n'; output += '| ----------- | -- | ----- | -------- | ------- |\n'; const sortedVulns = _.orderBy( vulnerabilities, ['score', 'name'], ['desc', 'asc'] ); for (const vuln of sortedVulns) { const severity = `${vuln.score ?? '?'} (${vuln.severity .charAt(0) .toUpperCase()}${vuln.severity.slice(1)})`; const ignored = formatIgnored(vuln); output += `| ${vuln.packageName}@${vuln.packageVersion} | ${ vuln.id } | ${severity} | ${vuln.fixedIn.join(', ') || 'N/A'} | ${ignored} |\n`; } return output; } export async function generateVulnerabilityReport(options: { dependencyFiles: string[]; snykReports: string[]; snykPolicyPath?: string; failOn?: KnownSeverity; createJiraIssues?: boolean; printResult?: (result: string) => void; }): Promise<void> { const productionDependencies = await loadDependencyFiles( options.dependencyFiles ); const snykTestResult = await loadReports(options.snykReports); const rules = await loadSnykPolicyRules(options.snykPolicyPath); const applicableVulnerabilities = filterApplicableVulnerabilities( snykTestResult, productionDependencies, rules ); (options.printResult ?? console.info)( `## Vulnerabilities Report (${ applicableVulnerabilities.length } vulnerabilities) ${generateVulnerabilityTable(applicableVulnerabilities)}` ); if (options.createJiraIssues) { await createVulnerabilityTickets(applicableVulnerabilities); } if (options.failOn) { fail(options.failOn, applicableVulnerabilities); } } function commaSeparatedList(value: string) { return value.split(','); } export const command = new Command('generate-vulnerability-report') .description( 'Generate a report of snyk vulnerabilities applicable to a list of dependencies' ) .option( '--dependencies <paths>', 'Comma-separated list of dependency files', commaSeparatedList, [] ) .option( '--snyk-reports <paths>', 'Comma-separated list of snyk result files', commaSeparatedList, [] ) .option('--fail-on [level]', 'Fail on the specified severity level') .option('--snyk-policy-path [path]', 'Snyk policy path') .option( '--create-jira-issues', 'Create Jira issues for the vulnerabilities found' ) .action(async (options) => { await generateVulnerabilityReport({ dependencyFiles: options.dependencies, snykReports: options.snykReports, failOn: options.failOn, snykPolicyPath: options.snykPolicyPath, createJiraIssues: options.createJiraIssues, }); });