Compatibility with the latest cargo-audit output format

This commit is contained in:
svartalf
2020-01-26 20:19:18 +03:00
parent 35fc20ed69
commit 86207092bb
9 changed files with 1267 additions and 282 deletions
+1
View File
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Invalid input properly terminates Action execution (#1)
- Compatibility with new `cargo-audit` JSON output (#70)
## [1.0.0] - 2019-10-09
Vendored Executable
BIN
View File
Binary file not shown.
+1 -1
View File
File diff suppressed because one or more lines are too long
+991 -194
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "rust-audit-check",
"version": "1.0.0",
"version": "1.0.1",
"private": false,
"description": "Security audit for security vulnerabilities",
"main": "lib/main.js",
@@ -13,7 +13,7 @@
"watch": "ncc build src/main.ts --watch --minify",
"test": "jest --passWithNoTests",
"format": "prettier --write 'src/**/*.{js,ts,tsx}'",
"refresh": "rm -rf ./lib/* && npm run-script build",
"refresh": "rm -rf ./dist/* && npm run-script build",
"lint": "tsc --noEmit && eslint 'src/**/*.{js,ts,tsx}'"
},
"repository": {
@@ -37,8 +37,8 @@
"@actions-rs/core": "0.0.8",
"@actions/core": "^1.2.2",
"@actions/github": "^2.1.0",
"handlebars": "^4.7.2",
"npm-check-updates": "^4.0.1"
"npm-check-updates": "^4.0.1",
"nunjucks": "^3.2.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.17.0",
+29 -1
View File
@@ -1,8 +1,14 @@
/**
* These types should match to what `cargo-audit` outputs in a JSON format.
*
* See `rustsec` crate for structs used for serialization.
*/
export interface Report {
database: DatabaseInfo;
lockfile: LockfileInfo;
vulnerabilities: VulnerabilitiesInfo;
warnings: Vulnerability[];
warnings: Warning[];
}
export interface DatabaseInfo {
@@ -39,3 +45,25 @@ export interface Package {
name: string;
version: string;
}
export interface Warning {
kind: Kind;
package: Package;
}
// TypeScript types system is weird :(
export interface Kind {
unmaintained?: KindUnmaintained;
informational?: KindInformational;
yanked?: KindYanked;
}
export interface KindUnmaintained {
advisory: Advisory;
}
export interface KindInformational {
advisory: Advisory;
}
export interface KindYanked {} // eslint-disable-line @typescript-eslint/no-empty-interface
+3 -3
View File
@@ -67,17 +67,17 @@ export async function run(actionInput: input.Input): Promise<void> {
const client = new github.GitHub(actionInput.token, {
userAgent: USER_AGENT,
});
const advisories = report.vulnerabilities.list.concat(report.warnings);
const advisories = report.vulnerabilities.list;
if (github.context.eventName == 'schedule') {
core.debug(
'Action was triggered on a schedule event, creating an Issues report',
);
await reporter.reportIssues(client, advisories);
await reporter.reportIssues(client, advisories, report.warnings);
} else {
core.debug(
`Action was triggered on a ${github.context.eventName} event, creating a Check report`,
);
await reporter.reportCheck(client, advisories);
await reporter.reportCheck(client, advisories, report.warnings);
}
}
+121 -21
View File
@@ -2,6 +2,7 @@ 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 '@actions-rs/core';
import * as interfaces from './interfaces';
@@ -14,21 +15,54 @@ interface Stats {
other: number;
}
function dumpVulnerabilities(
vulnerabilities: Array<interfaces.Vulnerability>,
): void {
const render = templates.CHECK_TEXT({
vulnerabilities: vulnerabilities,
nunjucks.configure({
trimBlocks: true,
lstripBlocks: true,
});
core.info(render);
function makeReport(
vulnerabilities: Array<interfaces.Vulnerability>,
warnings: Array<interfaces.Warning>,
): string {
const preparedWarnings: Array<templates.ReportWarning> = [];
for (const warning of warnings) {
// TODO: Is there any better way?
if ('unmaintained' in warning.kind) {
preparedWarnings.push({
advisory: warning.kind.unmaintained!.advisory, // eslint-disable-line @typescript-eslint/no-non-null-assertion
package: warning.package,
});
} else if ('informational' in warning.kind) {
preparedWarnings.push({
advisory: warning.kind.informational!.advisory, // eslint-disable-line @typescript-eslint/no-non-null-assertion
package: warning.package,
});
} else if ('yanked' in warning.kind) {
preparedWarnings.push({
package: warning.package,
});
} else {
core.warning(
`Unknown warning kind ${warning.kind} found, please, file a bug`,
);
continue;
}
}
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>): Stats {
function getStats(
vulnerabilities: Array<interfaces.Vulnerability>,
warnings: Array<interfaces.Warning>,
): Stats {
let critical = 0;
let notices = 0;
let unmaintained = 0;
@@ -50,6 +84,15 @@ function getStats(vulnerabilities: Array<interfaces.Vulnerability>): Stats {
}
}
for (const warning of warnings) {
if (warning.kind.unmaintained) {
unmaintained += 1;
} else {
// Both yanked and informational types of kind
other += 1;
}
}
return {
critical: critical,
notices: notices,
@@ -82,9 +125,10 @@ function getSummary(stats: Stats): string {
export async function reportCheck(
client: github.GitHub,
vulnerabilities: Array<interfaces.Vulnerability>,
warnings: Array<interfaces.Warning>,
): Promise<void> {
const reporter = new checks.CheckReporter(client, 'Security audit');
const stats = getStats(vulnerabilities);
const stats = getStats(vulnerabilities, warnings);
const summary = getSummary(stats);
core.info(`Found ${summary}`);
@@ -104,7 +148,7 @@ 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);
core.info(makeReport(vulnerabilities, warnings));
if (stats.critical > 0) {
throw new Error(
'Critical vulnerabilities were found, marking check as failed',
@@ -116,12 +160,11 @@ See https://github.com/actions-rs/clippy-check/issues/2 for details.`);
}
try {
const body = makeReport(vulnerabilities, warnings);
const output = {
title: 'Security advisories found',
summary: summary,
text: templates.CHECK_TEXT({
vulnerabilities: vulnerabilities,
}),
text: body,
};
const status = stats.critical > 0 ? 'failure' : 'success';
await reporter.finishCheck(status, output);
@@ -137,35 +180,92 @@ See https://github.com/actions-rs/clippy-check/issues/2 for details.`);
}
}
export async function reportIssues(
async function alreadyReported(
client: github.GitHub,
vulnerabilities: Array<interfaces.Vulnerability>,
): Promise<void> {
advisoryId: string,
): Promise<boolean> {
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}`,
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 ${vulnerability.advisory.id} is mentioned already in the issues/PRs, \
`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(
client: github.GitHub,
vulnerabilities: Array<interfaces.Vulnerability>,
warnings: Array<interfaces.Warning>,
): Promise<void> {
const { owner, repo } = github.context.repo;
for (const vulnerability of vulnerabilities) {
const reported = await alreadyReported(
client,
vulnerability.advisory.id,
);
if (reported) {
continue;
}
const body = nunjucks.renderString(templates.VULNERABILITY_ISSUE, {
vulnerability: vulnerability,
});
const issue = await client.issues.create({
owner: owner,
repo: repo,
title: `${vulnerability.advisory.id}: ${vulnerability.advisory.title}`,
body: templates.ISSUE_BODY({
vulnerability: vulnerability,
}),
body: body,
});
core.info(
`Created an issue for ${vulnerability.advisory.id}: ${issue.data.html_url}`,
);
}
for (const warning of warnings) {
let advisory: interfaces.Advisory;
if ('unmaintained' in warning.kind) {
advisory = warning.kind.unmaintained!.advisory; // eslint-disable-line @typescript-eslint/no-non-null-assertion
} else if ('informational' in warning.kind) {
advisory = warning.kind.informational!.advisory; // eslint-disable-line @typescript-eslint/no-non-null-assertion
} else if ('yanked' in warning.kind) {
core.warning(
`Crate ${warning.package.name} was yanked, but no issue will be reported about it`,
);
continue;
} else {
core.warning(
`Unknown warning kind ${warning.kind} found, please, file a bug`,
);
continue;
}
const reported = await alreadyReported(client, advisory.id);
if (reported) {
continue;
}
const body = nunjucks.renderString(templates.WARNING_ISSUE, {
warning: warning,
advisory: advisory,
});
const issue = await client.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}`,
);
}
}
+105 -46
View File
@@ -1,64 +1,123 @@
import * as Handlebars from 'handlebars';
/**
* Naive way to bundle the templates in order to skip any build/watch pre-processing steps.
*/
export const CHECK_TEXT = Handlebars.compile(
`
{{#each vulnerabilities}}
## [{{this.advisory.id}}](https://rustsec.org/advisories/{{this.advisory.id}}.html)
import * as interfaces from './interfaces';
> {{this.advisory.title}}
export interface ReportWarning {
advisory?: interfaces.Advisory;
package: interfaces.Package;
}
export const REPORT = `
{% if vulnerabilities.length > 0 %}
## Vulnerabilities
{% for v in vulnerabilities %}
### [{{ v.advisory.id }}](https://rustsec.org/advisories/{{ v.advisory.id }}.html)
> {{ v.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,
},
);
{% if v.advisory.informational %}
| Status | {{ v.advisory.informational }} |
{% endif %}
| Package | \`{{ v.package.name }}\` |
| Version | \`{{ v.package.version }}\` |
{% if v.advisory.url %}
| URL | [{{ v.advisory.url }}]({{ v.advisory.url }}) |
{% endif %}
| Date | {{ v.advisory.date }} |
{% if v.versions.patched.length > 0 %}
| Patched versions | \`{{ v.versions.patched | safe }}\` |
{% endif %}
{% if v.versions.unaffected.length > 0 %}
| Unaffected versions | \`{{ v.versions.unaffected | safe }}\` |
{% endif %}
export const ISSUE_BODY = Handlebars.compile(
`
{{ v.advisory.description }}
{% endfor %}
{% endif %}
{% if warnings.length > 0 %}
## Warnings
{% for w in warnings %}
{% if w.advisory %}
### [{{ w.advisory.id }}](https://rustsec.org/advisories/{{ w.advisory.id }}.html)
> {{ w.advisory.title }}
| Details | |
| ------------------- | ---------------------------------------------- |
{% if w.advisory.informational %}
| Status | {{ w.advisory.informational }} |
{% endif %}
| Package | \`{{ w.package.name }}\` |
| Version | \`{{ w.package.version | safe }}\` |
{% if w.advisory.url %}
| URL | [{{ w.advisory.url }}]({{ w.advisory.url }}) |
{% endif %}
| Date | {{ w.advisory.date }} |
{% if w.versions.patched.length > 0 %}
| Patched versions | \`{{ w.versions.patched | safe }}\` |
{% endif %}
{% if w.versions.unaffected.length > 0 %}
| Unaffected versions | \`{{ w.versions.unaffected | safe }}\` |
{% endif %}
{{ w.advisory.description }}
{% else %}
### Crate \`{{ w.package.name }}\` is yanked
No extra details provided.
{% endif %}
{% endfor %}
{% endif %}
`;
export const VULNERABILITY_ISSUE = `
> {{ vulnerability.advisory.title }}
| Details | |
| ------------------- | ---------------------------------------------- |
{{#if vulnerability.advisory.informational}}
{% if vulnerability.advisory.informational %}
| Status | {{ vulnerability.advisory.informational }} |
{{/if}}
{% endif %}
| Package | \`{{ vulnerability.package.name }}\` |
| Version | \`{{vulnerability.package.version}}\` |
| Version | \`{{ vulnerability.package.version | safe }}\` |
{% if vulnerability.advisory.url %}
| URL | [{{ vulnerability.advisory.url }}]({{ vulnerability.advisory.url }}) |
{% endif %}
| Date | {{ vulnerability.advisory.date }} |
{{#if vulnerability.versions.patched}}
| Patched versions | \`{{vulnerability.versions.patched}}\` |
{{/if}}
{{#if vulnerability.versions.unaffected}}
| Unaffected versions | \`{{vulnerability.versions.unaffected}}\` |
{{/if}}
{% if vulnerability.versions.patched.length > 0 %}
| Patched versions | \`{{ vulnerability.versions.patched | safe }}\` |
{% endif %}
{% if vulnerability.versions.unaffected.length > 0 %}
| Unaffected versions | \`{{ vulnerability.versions.unaffected | safe }}\` |
{% endif %}
{{ vulnerability.advisory.description }}
See [advisory page](https://rustsec.org/advisories/{{ vulnerability.advisory.id }}.html) for additional details.
`,
{
knownHelpersOnly: true,
noEscape: true,
strict: true,
},
);
`;
export const WARNING_ISSUE = `
> {{ advisory.title }}
| Details | |
| ------------------- | ---------------------------------------------- |
{% if advisory.informational %}
| Status | {{ advisory.informational }} |
{% endif %}
| Package | \`{{ warning.package.name }}\` |
| Version | \`{{ warning.package.version | safe }}\` |
| URL | [{{ advisory.url }}]({{ advisory.url }}) |
| Date | {{ advisory.date }} |
{{ advisory.description }}
See [advisory page](https://rustsec.org/advisories/{{ advisory.id }}.html) for additional details.
`;