Public Access
Compatibility with the latest cargo-audit output format
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
BIN
Binary file not shown.
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Generated
+991
-194
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user