Public Access
Release v1.0.0
This commit is contained in:
@@ -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 }),
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user