Release v1.0.0

This commit is contained in:
svartalf
2019-10-09 19:29:13 +03:00
parent c2c3be075e
commit 16efef378e
25 changed files with 6668 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
/**
* Parse action input into a some proper thing.
*/
import { input } from '@actions-rs/core';
// Parsed action input
export interface Input {
token: string;
}
export function get(): Input {
return {
token: input.getInput('token', { required: true }),
};
}
+41
View File
@@ -0,0 +1,41 @@
export interface Report {
database: DatabaseInfo;
lockfile: LockfileInfo;
vulnerabilities: VulnerabilitiesInfo;
warnings: Vulnerability[];
}
export interface DatabaseInfo {
'advisory-count': number;
'last-commit': string;
'last-updated': string;
}
export interface LockfileInfo {
'dependency-count': number;
}
export interface VulnerabilitiesInfo {
found: boolean;
count: number;
list: Vulnerability[];
}
export interface Vulnerability {
advisory: Advisory;
package: Package;
}
export interface Advisory {
id: string;
package: string;
title: string;
description: string;
informational: undefined | string | 'notice' | 'unmaintained';
url: string;
}
export interface Package {
name: string;
version: string;
}
+96
View File
@@ -0,0 +1,96 @@
import * as process from 'process';
import * as os from 'os';
import * as core from '@actions/core';
import * as github from '@actions/github';
import { Cargo } from '@actions-rs/core';
import * as input from './input';
import * as interfaces from './interfaces';
import * as reporter from './reporter';
const pkg = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
const USER_AGENT = `${pkg.name}/${pkg.version} (${pkg.bugs.url})`;
async function getData(): Promise<interfaces.Report> {
const cargo = await Cargo.get();
await cargo.findOrInstall('cargo-audit');
await cargo.call(['generate-lockfile']);
let stdout = '';
try {
core.startGroup('Calling cargo-audit (JSON output)');
await cargo.call(['audit', '--json'], {
ignoreReturnCode: true,
listeners: {
stdout: buffer => {
stdout += buffer.toString();
},
},
});
} finally {
// Cool story: `cargo-audit` JSON output is missing the trailing `\n`,
// so the `::endgroup::` annotation from the line below is being
// eaten by it.
// Manually writing the `\n` to denote the `cargo-audit` end
process.stdout.write(os.EOL);
core.endGroup();
}
return JSON.parse(stdout);
}
export async function run(actionInput: input.Input): Promise<void> {
const report = await getData();
let shouldReport = false;
if (!report.vulnerabilities.found) {
core.info('No vulnerabilities were found');
} else {
core.warning(`${report.vulnerabilities.count} vulnerabilities found!`);
shouldReport = true;
}
if (report.warnings.length === 0) {
core.info('No warnings were found');
} else {
core.warning(`${report.warnings.length} warnings found!`);
shouldReport = true;
}
if (!shouldReport) {
return;
}
const client = new github.GitHub(actionInput.token, {
userAgent: USER_AGENT,
});
const advisories = report.vulnerabilities.list.concat(report.warnings);
if (github.context.eventName == 'schedule') {
core.debug(
'Action was triggered on a schedule event, creating an Issues report',
);
await reporter.reportIssues(client, advisories);
} else {
core.debug(
`Action was triggered on a ${github.context.eventName} event, creating a Check report`,
);
await reporter.reportCheck(client, advisories);
}
}
async function main(): Promise<void> {
const actionInput = input.get();
try {
await run(actionInput);
} catch (error) {
core.setFailed(error.message);
}
return;
}
main();
+171
View File
@@ -0,0 +1,171 @@
import * as process from 'process';
import * as core from '@actions/core';
import * as github from '@actions/github';
import { checks } from '@actions-rs/core';
import * as interfaces from './interfaces';
import * as templates from './templates';
interface Stats {
critical: number;
notices: number;
unmaintained: number;
other: number;
}
function dumpVulnerabilities(
vulnerabilities: Array<interfaces.Vulnerability>,
): void {
const render = templates.CHECK_TEXT({
vulnerabilities: vulnerabilities,
});
core.info(render);
}
export function plural(value: number, suffix = 's'): string {
return value == 1 ? '' : suffix;
}
function getStats(vulnerabilities: Array<interfaces.Vulnerability>): 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;
}
}
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(
client: github.GitHub,
vulnerabilities: Array<interfaces.Vulnerability>,
): Promise<void> {
const reporter = new checks.CheckReporter(client, 'Security audit');
const stats = getStats(vulnerabilities);
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.');
dumpVulnerabilities(vulnerabilities);
if (stats.critical > 0) {
throw new Error(
'Critical vulnerabilities were found, marking check as failed',
);
}
}
throw error;
}
try {
const output = {
title: 'Security advisories found',
summary: summary,
text: templates.CHECK_TEXT({
vulnerabilities: vulnerabilities,
}),
};
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',
);
}
}
export async function reportIssues(
client: github.GitHub,
vulnerabilities: Array<interfaces.Vulnerability>,
): Promise<void> {
const { owner, repo } = github.context.repo;
for (const vulnerability of vulnerabilities) {
const results = await client.search.issuesAndPullRequests({
q: `${vulnerability.advisory.id} in:title repo:${owner}/${repo}`,
per_page: 1, // eslint-disable-line @typescript-eslint/camelcase
});
if (results.data.total_count > 0) {
core.info(
`Seems like ${vulnerability.advisory.id} is mentioned already in the issues/PRs, \
will not report an issue against it`,
);
continue;
}
const issue = await client.issues.create({
owner: owner,
repo: repo,
title: `${vulnerability.advisory.id}: ${vulnerability.advisory.title}`,
body: templates.ISSUE_BODY({
vulnerability: vulnerability,
}),
});
core.info(
`Created an issue for ${vulnerability.advisory.id}: ${issue.data.html_url}`,
);
}
}
+64
View File
@@ -0,0 +1,64 @@
import * as Handlebars from 'handlebars';
export const CHECK_TEXT = Handlebars.compile(
`
{{#each vulnerabilities}}
## [{{this.advisory.id}}](https://rustsec.org/advisories/{{this.advisory.id}}.html)
> {{this.advisory.title}}
| Details | |
| ------------------- | ---------------------------------------------- |
{{#if this.advisory.informational}}
| Status | {{this.advisory.informational}} |
{{/if}}
| Package | \`{{this.package.name}}\` |
| Version | \`{{this.package.version}}\` |
| URL | [{{this.advisory.url}}]({{this.advisory.url}}) |
| Date | {{this.advisory.date}} |
{{#if this.versions.patched.length}}
| Patched versions | \`{{this.versions.patched}}\` |
{{/if}}
{{#if this.versions.unaffected.length}}
| Unaffected versions | \`{{this.versions.unaffected}}\` |
{{/if}}
{{this.advisory.description}}
{{/each}}
`,
{
knownHelpersOnly: true,
noEscape: true,
strict: true,
},
);
export const ISSUE_BODY = Handlebars.compile(
`
> {{vulnerability.advisory.title}}
| Details | |
| ------------------- | ---------------------------------------------- |
{{#if vulnerability.advisory.informational}}
| Status | {{vulnerability.advisory.informational}} |
{{/if}}
| Package | \`{{vulnerability.package.name}}\` |
| Version | \`{{vulnerability.package.version}}\` |
| URL | [{{vulnerability.advisory.url}}]({{vulnerability.advisory.url}}) |
| Date | {{vulnerability.advisory.date}} |
{{#if vulnerability.versions.patched}}
| Patched versions | \`{{vulnerability.versions.patched}}\` |
{{/if}}
{{#if vulnerability.versions.unaffected}}
| Unaffected versions | \`{{vulnerability.versions.unaffected}}\` |
{{/if}}
{{vulnerability.advisory.description}}
See [advisory page](https://rustsec.org/advisories/{{vulnerability.advisory.id}}.html) for additional details.
`,
{
knownHelpersOnly: true,
noEscape: true,
strict: true,
},
);