ipsambeatae
Version:
Shared dependencies of Compass, the MongoDB extension for VSCode and MongoSH
229 lines (195 loc) • 6.08 kB
text/typescript
/* 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,
});
});