Files
rust-audit-check/src/reporter.ts
T
pinkforest(she/her) 7cb7a4e8d4
Continuous integration / main (push) Successful in 2m12s
Add notice and unsound (#9)
2023-04-04 08:08:08 +10:00

314 lines
9.0 KiB
TypeScript

import * as process from 'process';
import * as core from '@actions/core';
import * as github from '@actions/github';
import * as nunjucks from 'nunjucks';
import { checks } from '@rinse-repeat/actions-rs-core';
import * as interfaces from './interfaces';
import * as templates from './templates';
const pkg = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
const USER_AGENT = `${pkg.name}/${pkg.version} (${pkg.bugs.url})`;
interface Stats {
critical: number;
notices: number;
unmaintained: number;
other: number;
}
nunjucks.configure({
trimBlocks: true,
lstripBlocks: true,
});
function makeReport(
vulnerabilities: Array<interfaces.Vulnerability>,
warnings: Array<interfaces.Warning>,
): string {
const preparedWarnings: Array<templates.ReportWarning> = [];
for (const warning of warnings) {
switch (warning.kind) {
case 'unmaintained':
preparedWarnings.push({
advisory: warning.advisory,
package: warning.package,
});
break;
case 'unsound':
preparedWarnings.push({
advisory: warning.advisory,
package: warning.package,
});
break;
case 'notice':
preparedWarnings.push({
advisory: warning.advisory,
package: warning.package,
});
break;
case 'informational':
preparedWarnings.push({
advisory: warning.advisory,
package: warning.package,
});
break;
case 'yanked':
preparedWarnings.push({
package: warning.package,
});
break;
default:
core.warning(
`Unknown warning kind ${warning.kind} found, please, file a bug`,
);
break;
}
}
return nunjucks.renderString(templates.REPORT, {
vulnerabilities: vulnerabilities,
warnings: preparedWarnings,
});
}
export function plural(value: number, suffix = 's'): string {
return value == 1 ? '' : suffix;
}
function getStats(
vulnerabilities: Array<interfaces.Vulnerability>,
warnings: Array<interfaces.Warning>,
): Stats {
let critical = 0;
let notices = 0;
let unmaintained = 0;
let other = 0;
for (const vulnerability of vulnerabilities) {
switch (vulnerability.advisory.informational) {
case 'notice':
notices += 1;
break;
case 'unmaintained':
unmaintained += 1;
break;
case null:
critical += 1;
break;
default:
other += 1;
break;
}
}
for (const warning of warnings) {
switch (warning.kind) {
case 'unmaintained':
unmaintained += 1;
break;
default:
// Both yanked and informational types of kind
other += 1;
break;
}
}
return {
critical: critical,
notices: notices,
unmaintained: unmaintained,
other: other,
};
}
function getSummary(stats: Stats): string {
const blocks: string[] = [];
if (stats.critical > 0) {
// TODO: Plural
blocks.push(`${stats.critical} advisory(ies)`);
}
if (stats.notices > 0) {
blocks.push(`${stats.notices} notice${plural(stats.notices)}`);
}
if (stats.unmaintained > 0) {
blocks.push(`${stats.unmaintained} unmaintained`);
}
if (stats.other > 0) {
blocks.push(`${stats.other} other`);
}
return blocks.join(', ');
}
/// Create and publish audit results into the Commit Check.
export async function reportCheck(
token: string,
vulnerabilities: Array<interfaces.Vulnerability>,
warnings: Array<interfaces.Warning>,
): Promise<void> {
const client = github.getOctokit(token, {userAgent: USER_AGENT});
const reporter = new checks.CheckReporter(client.rest, 'Security audit');
const stats = getStats(vulnerabilities, warnings);
const summary = getSummary(stats);
core.info(`Found ${summary}`);
try {
await reporter.startCheck('queued');
} catch (error) {
// `GITHUB_HEAD_REF` is set only for forked repos,
// so we can check if it is a fork and not a base repo.
if (process.env.GITHUB_HEAD_REF) {
core.error(`Unable to publish audit check! Reason: ${error}`);
core.warning(
'It seems that this Action is executed from the forked repository.',
);
core.warning(`GitHub Actions are not allowed to use Check API, \
when executed for a forked repos. \
See https://github.com/actions-rs/clippy-check/issues/2 for details.`);
core.info('Posting audit report here instead.');
core.info(makeReport(vulnerabilities, warnings));
if (stats.critical > 0) {
throw new Error(
'Critical vulnerabilities were found, marking check as failed',
);
} else {
core.info(
'No critical vulnerabilities were found, not marking check as failed',
);
return;
}
}
throw error;
}
try {
const body = makeReport(vulnerabilities, warnings);
const output = {
title: 'Security advisories found',
summary: summary,
text: body,
};
const status = stats.critical > 0 ? 'failure' : 'success';
await reporter.finishCheck(status, output);
} catch (error) {
await reporter.cancelCheck();
throw error;
}
if (stats.critical > 0) {
throw new Error(
'Critical vulnerabilities were found, marking check as failed',
);
} else {
core.info(
'No critical vulnerabilities were found, not marking check as failed',
);
return;
}
}
async function alreadyReported(
token: string,
advisoryId: string,
): Promise<boolean> {
const { owner, repo } = github.context.repo;
const client = github.getOctokit(token, {userAgent: USER_AGENT});
const results = await client.rest.search.issuesAndPullRequests({
q: `${advisoryId} in:title repo:${owner}/${repo}`,
per_page: 1, // eslint-disable-line @typescript-eslint/camelcase
});
if (results.data.total_count > 0) {
core.info(
`Seems like ${advisoryId} is mentioned already in the issues/PRs, \
will not report an issue against it`,
);
return true;
} else {
return false;
}
}
export async function reportIssues(
token: string,
vulnerabilities: Array<interfaces.Vulnerability>,
warnings: Array<interfaces.Warning>,
): Promise<void> {
const { owner, repo } = github.context.repo;
const client = github.getOctokit(token, {userAgent: USER_AGENT});
for (const vulnerability of vulnerabilities) {
const reported = await alreadyReported(
token,
vulnerability.advisory.id,
);
if (reported) {
continue;
}
const body = nunjucks.renderString(templates.VULNERABILITY_ISSUE, {
vulnerability: vulnerability,
});
const issue = await client.rest.issues.create({
owner: owner,
repo: repo,
title: `${vulnerability.advisory.id}: ${vulnerability.advisory.title}`,
body: body,
});
core.info(
`Created an issue for ${vulnerability.advisory.id}: ${issue.data.html_url}`,
);
}
for (const warning of warnings) {
let advisory: interfaces.Advisory;
switch (warning.kind) {
case 'unmaintained':
case 'informational':
advisory = warning.advisory;
break;
case 'yanked':
core.warning(
`Crate ${warning.package.name} was yanked, but no issue will be reported about it`,
);
continue;
default:
core.warning(
`Unknown warning kind ${warning.kind} found, please, file a bug`,
);
continue;
}
const reported = await alreadyReported(token, advisory.id);
if (reported) {
continue;
}
const body = nunjucks.renderString(templates.WARNING_ISSUE, {
warning: warning,
advisory: advisory,
});
const issue = await client.rest.issues.create({
owner: owner,
repo: repo,
title: `${advisory.id}: ${advisory.title}`,
body: body,
});
core.info(
`Created an issue for ${advisory.id}: ${issue.data.html_url}`,
);
}
}