├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── check_screenshot.png ├── issue_screenshot.png └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── action.yml ├── dist ├── fsevents.node └── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── input.ts ├── interfaces.ts ├── main.ts ├── reporter.ts └── templates.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | max_line_length = 80 8 | indent_size = 4 9 | 10 | [*.yml] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parser": "@typescript-eslint/parser", 6 | "parserOptions": { 7 | "project": "./tsconfig.json" 8 | }, 9 | "plugins": [ 10 | "@typescript-eslint" 11 | ], 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:prettier/recommended", 18 | "prettier", 19 | "prettier/@typescript-eslint" 20 | ], 21 | "rules": { 22 | "@typescript-eslint/explicit-function-return-type": 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: svartalf 2 | patreon: svartalf 3 | custom: ["https://svartalf.info/donate/", "https://www.buymeacoffee.com/svartalf"] 4 | -------------------------------------------------------------------------------- /.github/check_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actions-rs/audit-check/35b7b53b1e25b55642157ac01b4adceb5b9ebef3/.github/check_screenshot.png -------------------------------------------------------------------------------- /.github/issue_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actions-rs/audit-check/35b7b53b1e25b55642157ac01b4adceb5b9ebef3/.github/issue_screenshot.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | main: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Create npm configuration 10 | run: echo "//npm.pkg.github.com/:_authToken=${token}" >> ~/.npmrc 11 | env: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | 14 | - uses: actions/checkout@v1 15 | - run: npm ci 16 | - run: npm run lint 17 | - run: npm run build 18 | - run: npm run test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __tests__/runner/* 2 | 3 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @actions-rs:registry=https://npm.pkg.github.com 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.2.0] - 2020-05-07 9 | 10 | ### Fixed 11 | 12 | - Compatibility with latest `cargo-audit == 0.12` JSON output (#115) 13 | - Do not fail check if no critical vulnerabilities were found when executed for a fork repository (closes #104) 14 | 15 | ## [1.1.0] 16 | 17 | ### Fixed 18 | 19 | - Invalid input properly terminates Action execution (#1) 20 | - Compatibility with new `cargo-audit` JSON output (#70) 21 | 22 | ## [1.0.0] - 2019-10-09 23 | 24 | ### Added 25 | 26 | - First public version 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 actions-rs team and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust `audit-check` Action 2 | 3 | ![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg) 4 | [![Gitter](https://badges.gitter.im/actions-rs/community.svg)](https://gitter.im/actions-rs/community) 5 | 6 | > Security vulnerabilities audit 7 | 8 | This GitHub Action is using [cargo-audit](https://github.com/RustSec/cargo-audit) 9 | to perform an audit for crates with security vulnerabilities. 10 | 11 | ## Usage 12 | 13 | ### Audit changes 14 | 15 | We can utilize the GitHub Actions ability to execute workflow 16 | only if [the specific files were changed](https://help.github.com/en/articles/workflow-syntax-for-github-actions#onpushpull_requestpaths) 17 | and execute this Action to check the changed dependencies: 18 | 19 | ```yaml 20 | name: Security audit 21 | on: 22 | push: 23 | paths: 24 | - '**/Cargo.toml' 25 | - '**/Cargo.lock' 26 | jobs: 27 | security_audit: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v1 31 | - uses: actions-rs/audit-check@v1 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | ``` 35 | 36 | It is recommended to add the `paths:` section into the workflow file, 37 | as it would effectively speed up the CI pipeline, since the audit process 38 | will not be performed if no dependencies were changed. 39 | 40 | 41 | In case of any security advisories found, [status check](https://help.github.com/en/articles/about-status-checks) 42 | created by this Action will be marked as "failed".\ 43 | Note that informational advisories are not affecting the check status. 44 | 45 | ![Check screenshot](.github/check_screenshot.png) 46 | 47 | #### Limitations 48 | 49 | Due to [token permissions](https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions), 50 | this Action **WILL NOT** be able to create Checks for Pull Requests from the forked repositories, 51 | see [actions-rs/clippy-check#2](https://github.com/actions-rs/clippy-check/issues/2) for details.\ 52 | As a fallback this Action will output all found advisories to the stdout.\ 53 | It is expected that this behavior will be fixed later by GitHub. 54 | 55 | ## Scheduled audit 56 | 57 | Another option is to use [`schedule`](https://help.github.com/en/articles/events-that-trigger-workflows#scheduled-events-schedule) event 58 | and execute this Action periodically against the `HEAD` of repository default branch. 59 | 60 | ```yaml 61 | name: Security audit 62 | on: 63 | schedule: 64 | - cron: '0 0 * * *' 65 | jobs: 66 | audit: 67 | runs-on: ubuntu-latest 68 | steps: 69 | - uses: actions/checkout@v1 70 | - uses: actions-rs/audit-check@v1 71 | with: 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | ``` 74 | 75 | With this example Action will be executed periodically at midnight of each day 76 | and check if there any new advisories appear for crate dependencies.\ 77 | For each new advisory (including informal) an issue will be created: 78 | 79 | ![Issue screenshot](.github/issue_screenshot.png) 80 | 81 | ## Inputs 82 | 83 | | Name | Required | Description | Type | Default | 84 | | ------------| -------- | ---------------------------------------------------------------------------| ------ | --------| 85 | | `token` | ✓ | [GitHub token], usually a `${{ secrets.GITHUB_TOKEN }}` | string | | 86 | 87 | [GitHub token]: https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token 88 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'rust-audit-check' 2 | description: 'Run cargo audit and check for security advisories' 3 | author: 'actions-rs team' 4 | branding: 5 | icon: play-circle 6 | color: black 7 | inputs: 8 | token: 9 | description: GitHub Actions token 10 | required: true 11 | 12 | runs: 13 | using: 'node12' 14 | main: 'dist/index.js' 15 | -------------------------------------------------------------------------------- /dist/fsevents.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/actions-rs/audit-check/35b7b53b1e25b55642157ac01b4adceb5b9ebef3/dist/fsevents.node -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | "\\.(ts|js)x?$": "ts-jest" 9 | }, 10 | verbose: true 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rust-audit-check", 3 | "version": "1.2.0", 4 | "private": false, 5 | "description": "Security audit for security vulnerabilities", 6 | "main": "lib/main.js", 7 | "directories": { 8 | "lib": "lib", 9 | "test": "__tests__" 10 | }, 11 | "scripts": { 12 | "build": "ncc build src/main.ts --minify", 13 | "watch": "ncc build src/main.ts --watch --minify", 14 | "test": "jest --passWithNoTests", 15 | "format": "prettier --write 'src/**/*.{js,ts,tsx}'", 16 | "refresh": "rm -rf ./dist/* && npm run-script build", 17 | "lint": "tsc --noEmit && eslint 'src/**/*.{js,ts,tsx}'" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/actions-rs/audit.git" 22 | }, 23 | "keywords": [ 24 | "actions", 25 | "rust", 26 | "cargo", 27 | "audit", 28 | "security", 29 | "advisory" 30 | ], 31 | "author": "actions-rs", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/actions-rs/audit-check/issues" 35 | }, 36 | "dependencies": { 37 | "@actions-rs/core": "0.0.9", 38 | "@actions/core": "^1.2.4", 39 | "@actions/github": "^2.1.1", 40 | "npm-check-updates": "^4.1.2", 41 | "nunjucks": "^3.2.1" 42 | }, 43 | "devDependencies": { 44 | "@typescript-eslint/eslint-plugin": "^2.31.0", 45 | "ts-node": "^8.10.1", 46 | "@typescript-eslint/parser": "^2.31.0", 47 | "eslint": "^6.8.0", 48 | "eslint-config-prettier": "^6.11.0", 49 | "eslint-plugin-prettier": "^3.1.3", 50 | "@types/jest": "^25.2.1", 51 | "@types/node": "^13.13.5", 52 | "@zeit/ncc": "^0.22.1", 53 | "jest": "^26.0.1", 54 | "jest-circus": "^26.0.1", 55 | "ts-jest": "^25.5.0", 56 | "typescript": "^3.8.3", 57 | "prettier": "^2.0.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/input.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse action input into a some proper thing. 3 | */ 4 | 5 | import { input } from '@actions-rs/core'; 6 | 7 | // Parsed action input 8 | export interface Input { 9 | token: string; 10 | } 11 | 12 | export function get(): Input { 13 | return { 14 | token: input.getInput('token', { required: true }), 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * These types should match to what `cargo-audit` outputs in a JSON format. 3 | * 4 | * See `rustsec` crate for structs used for serialization. 5 | */ 6 | 7 | export interface Report { 8 | database: DatabaseInfo; 9 | lockfile: LockfileInfo; 10 | vulnerabilities: VulnerabilitiesInfo; 11 | warnings: Warning[] | { [key: string]: Warning[] }; 12 | } 13 | 14 | export interface DatabaseInfo { 15 | 'advisory-count': number; 16 | 'last-commit': string; 17 | 'last-updated': string; 18 | } 19 | 20 | export interface LockfileInfo { 21 | 'dependency-count': number; 22 | } 23 | 24 | export interface VulnerabilitiesInfo { 25 | found: boolean; 26 | count: number; 27 | list: Vulnerability[]; 28 | } 29 | 30 | export interface Vulnerability { 31 | advisory: Advisory; 32 | package: Package; 33 | } 34 | 35 | export interface Advisory { 36 | id: string; 37 | package: string; 38 | title: string; 39 | description: string; 40 | informational: undefined | string | 'notice' | 'unmaintained'; 41 | url: string; 42 | } 43 | 44 | export interface Package { 45 | name: string; 46 | version: string; 47 | } 48 | 49 | export interface Warning { 50 | kind: 'unmaintained' | 'informational' | 'yanked' | string; 51 | advisory: Advisory; 52 | package: Package; 53 | } 54 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as process from 'process'; 2 | import * as os from 'os'; 3 | 4 | import * as core from '@actions/core'; 5 | import * as github from '@actions/github'; 6 | 7 | import { Cargo } from '@actions-rs/core'; 8 | 9 | import * as input from './input'; 10 | import * as interfaces from './interfaces'; 11 | import * as reporter from './reporter'; 12 | 13 | const pkg = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires 14 | 15 | const USER_AGENT = `${pkg.name}/${pkg.version} (${pkg.bugs.url})`; 16 | 17 | async function getData(): Promise { 18 | const cargo = await Cargo.get(); 19 | await cargo.findOrInstall('cargo-audit'); 20 | 21 | await cargo.call(['generate-lockfile']); 22 | 23 | let stdout = ''; 24 | try { 25 | core.startGroup('Calling cargo-audit (JSON output)'); 26 | await cargo.call(['audit', '--json'], { 27 | ignoreReturnCode: true, 28 | listeners: { 29 | stdout: (buffer) => { 30 | stdout += buffer.toString(); 31 | }, 32 | }, 33 | }); 34 | } finally { 35 | // Cool story: `cargo-audit` JSON output is missing the trailing `\n`, 36 | // so the `::endgroup::` annotation from the line below is being 37 | // eaten by it. 38 | // Manually writing the `\n` to denote the `cargo-audit` end 39 | process.stdout.write(os.EOL); 40 | core.endGroup(); 41 | } 42 | 43 | return JSON.parse(stdout); 44 | } 45 | 46 | export async function run(actionInput: input.Input): Promise { 47 | const report = await getData(); 48 | let shouldReport = false; 49 | if (!report.vulnerabilities.found) { 50 | core.info('No vulnerabilities were found'); 51 | } else { 52 | core.warning(`${report.vulnerabilities.count} vulnerabilities found!`); 53 | shouldReport = true; 54 | } 55 | 56 | // In `cargo-audit < 0.12` report contained an array of `Warning`. 57 | // In `cargo-audit >= 0.12` it is a JSON object, 58 | // where key is a warning type, and value is an array of `Warning` of that type. 59 | let warnings: Array = []; 60 | if (Array.isArray(report.warnings)) { 61 | warnings = report.warnings; 62 | } else { 63 | for (const items of Object.values(report.warnings)) { 64 | warnings = warnings.concat(items); 65 | } 66 | } 67 | 68 | if (warnings.length === 0) { 69 | core.info('No warnings were found'); 70 | } else { 71 | core.warning(`${warnings.length} warnings found!`); 72 | shouldReport = true; 73 | } 74 | 75 | if (!shouldReport) { 76 | return; 77 | } 78 | 79 | const client = new github.GitHub(actionInput.token, { 80 | userAgent: USER_AGENT, 81 | }); 82 | const advisories = report.vulnerabilities.list; 83 | if (github.context.eventName == 'schedule') { 84 | core.debug( 85 | 'Action was triggered on a schedule event, creating an Issues report', 86 | ); 87 | await reporter.reportIssues(client, advisories, warnings); 88 | } else { 89 | core.debug( 90 | `Action was triggered on a ${github.context.eventName} event, creating a Check report`, 91 | ); 92 | await reporter.reportCheck(client, advisories, warnings); 93 | } 94 | } 95 | 96 | async function main(): Promise { 97 | try { 98 | const actionInput = input.get(); 99 | await run(actionInput); 100 | } catch (error) { 101 | core.setFailed(error.message); 102 | } 103 | 104 | return; 105 | } 106 | 107 | main(); 108 | -------------------------------------------------------------------------------- /src/reporter.ts: -------------------------------------------------------------------------------- 1 | import * as process from 'process'; 2 | 3 | import * as core from '@actions/core'; 4 | import * as github from '@actions/github'; 5 | import * as nunjucks from 'nunjucks'; 6 | 7 | import { checks } from '@actions-rs/core'; 8 | import * as interfaces from './interfaces'; 9 | import * as templates from './templates'; 10 | 11 | interface Stats { 12 | critical: number; 13 | notices: number; 14 | unmaintained: number; 15 | other: number; 16 | } 17 | 18 | nunjucks.configure({ 19 | trimBlocks: true, 20 | lstripBlocks: true, 21 | }); 22 | 23 | function makeReport( 24 | vulnerabilities: Array, 25 | warnings: Array, 26 | ): string { 27 | const preparedWarnings: Array = []; 28 | for (const warning of warnings) { 29 | switch (warning.kind) { 30 | case 'unmaintained': 31 | preparedWarnings.push({ 32 | advisory: warning.advisory, 33 | package: warning.package, 34 | }); 35 | break; 36 | 37 | case 'informational': 38 | preparedWarnings.push({ 39 | advisory: warning.advisory, 40 | package: warning.package, 41 | }); 42 | break; 43 | 44 | case 'yanked': 45 | preparedWarnings.push({ 46 | package: warning.package, 47 | }); 48 | break; 49 | 50 | default: 51 | core.warning( 52 | `Unknown warning kind ${warning.kind} found, please, file a bug`, 53 | ); 54 | break; 55 | } 56 | } 57 | 58 | return nunjucks.renderString(templates.REPORT, { 59 | vulnerabilities: vulnerabilities, 60 | warnings: preparedWarnings, 61 | }); 62 | } 63 | 64 | export function plural(value: number, suffix = 's'): string { 65 | return value == 1 ? '' : suffix; 66 | } 67 | 68 | function getStats( 69 | vulnerabilities: Array, 70 | warnings: Array, 71 | ): Stats { 72 | let critical = 0; 73 | let notices = 0; 74 | let unmaintained = 0; 75 | let other = 0; 76 | for (const vulnerability of vulnerabilities) { 77 | switch (vulnerability.advisory.informational) { 78 | case 'notice': 79 | notices += 1; 80 | break; 81 | case 'unmaintained': 82 | unmaintained += 1; 83 | break; 84 | case null: 85 | critical += 1; 86 | break; 87 | default: 88 | other += 1; 89 | break; 90 | } 91 | } 92 | 93 | for (const warning of warnings) { 94 | switch (warning.kind) { 95 | case 'unmaintained': 96 | unmaintained += 1; 97 | break; 98 | 99 | default: 100 | // Both yanked and informational types of kind 101 | other += 1; 102 | break; 103 | } 104 | } 105 | 106 | return { 107 | critical: critical, 108 | notices: notices, 109 | unmaintained: unmaintained, 110 | other: other, 111 | }; 112 | } 113 | 114 | function getSummary(stats: Stats): string { 115 | const blocks: string[] = []; 116 | 117 | if (stats.critical > 0) { 118 | // TODO: Plural 119 | blocks.push(`${stats.critical} advisory(ies)`); 120 | } 121 | if (stats.notices > 0) { 122 | blocks.push(`${stats.notices} notice${plural(stats.notices)}`); 123 | } 124 | if (stats.unmaintained > 0) { 125 | blocks.push(`${stats.unmaintained} unmaintained`); 126 | } 127 | if (stats.other > 0) { 128 | blocks.push(`${stats.other} other`); 129 | } 130 | 131 | return blocks.join(', '); 132 | } 133 | 134 | /// Create and publish audit results into the Commit Check. 135 | export async function reportCheck( 136 | client: github.GitHub, 137 | vulnerabilities: Array, 138 | warnings: Array, 139 | ): Promise { 140 | const reporter = new checks.CheckReporter(client, 'Security audit'); 141 | const stats = getStats(vulnerabilities, warnings); 142 | const summary = getSummary(stats); 143 | 144 | core.info(`Found ${summary}`); 145 | 146 | try { 147 | await reporter.startCheck('queued'); 148 | } catch (error) { 149 | // `GITHUB_HEAD_REF` is set only for forked repos, 150 | // so we can check if it is a fork and not a base repo. 151 | if (process.env.GITHUB_HEAD_REF) { 152 | core.error(`Unable to publish audit check! Reason: ${error}`); 153 | core.warning( 154 | 'It seems that this Action is executed from the forked repository.', 155 | ); 156 | core.warning(`GitHub Actions are not allowed to use Check API, \ 157 | when executed for a forked repos. \ 158 | See https://github.com/actions-rs/clippy-check/issues/2 for details.`); 159 | core.info('Posting audit report here instead.'); 160 | 161 | core.info(makeReport(vulnerabilities, warnings)); 162 | if (stats.critical > 0) { 163 | throw new Error( 164 | 'Critical vulnerabilities were found, marking check as failed', 165 | ); 166 | } else { 167 | core.info( 168 | 'No critical vulnerabilities were found, not marking check as failed', 169 | ); 170 | return; 171 | } 172 | } 173 | 174 | throw error; 175 | } 176 | 177 | try { 178 | const body = makeReport(vulnerabilities, warnings); 179 | const output = { 180 | title: 'Security advisories found', 181 | summary: summary, 182 | text: body, 183 | }; 184 | const status = stats.critical > 0 ? 'failure' : 'success'; 185 | await reporter.finishCheck(status, output); 186 | } catch (error) { 187 | await reporter.cancelCheck(); 188 | throw error; 189 | } 190 | 191 | if (stats.critical > 0) { 192 | throw new Error( 193 | 'Critical vulnerabilities were found, marking check as failed', 194 | ); 195 | } else { 196 | core.info( 197 | 'No critical vulnerabilities were found, not marking check as failed', 198 | ); 199 | return; 200 | } 201 | } 202 | 203 | async function alreadyReported( 204 | client: github.GitHub, 205 | advisoryId: string, 206 | ): Promise { 207 | const { owner, repo } = github.context.repo; 208 | const results = await client.search.issuesAndPullRequests({ 209 | q: `${advisoryId} in:title repo:${owner}/${repo}`, 210 | per_page: 1, // eslint-disable-line @typescript-eslint/camelcase 211 | }); 212 | 213 | if (results.data.total_count > 0) { 214 | core.info( 215 | `Seems like ${advisoryId} is mentioned already in the issues/PRs, \ 216 | will not report an issue against it`, 217 | ); 218 | return true; 219 | } else { 220 | return false; 221 | } 222 | } 223 | 224 | export async function reportIssues( 225 | client: github.GitHub, 226 | vulnerabilities: Array, 227 | warnings: Array, 228 | ): Promise { 229 | const { owner, repo } = github.context.repo; 230 | 231 | for (const vulnerability of vulnerabilities) { 232 | const reported = await alreadyReported( 233 | client, 234 | vulnerability.advisory.id, 235 | ); 236 | if (reported) { 237 | continue; 238 | } 239 | 240 | const body = nunjucks.renderString(templates.VULNERABILITY_ISSUE, { 241 | vulnerability: vulnerability, 242 | }); 243 | const issue = await client.issues.create({ 244 | owner: owner, 245 | repo: repo, 246 | title: `${vulnerability.advisory.id}: ${vulnerability.advisory.title}`, 247 | body: body, 248 | }); 249 | core.info( 250 | `Created an issue for ${vulnerability.advisory.id}: ${issue.data.html_url}`, 251 | ); 252 | } 253 | 254 | for (const warning of warnings) { 255 | let advisory: interfaces.Advisory; 256 | switch (warning.kind) { 257 | case 'unmaintained': 258 | case 'informational': 259 | advisory = warning.advisory; 260 | break; 261 | case 'yanked': 262 | core.warning( 263 | `Crate ${warning.package.name} was yanked, but no issue will be reported about it`, 264 | ); 265 | continue; 266 | default: 267 | core.warning( 268 | `Unknown warning kind ${warning.kind} found, please, file a bug`, 269 | ); 270 | continue; 271 | } 272 | 273 | const reported = await alreadyReported(client, advisory.id); 274 | if (reported) { 275 | continue; 276 | } 277 | 278 | const body = nunjucks.renderString(templates.WARNING_ISSUE, { 279 | warning: warning, 280 | advisory: advisory, 281 | }); 282 | const issue = await client.issues.create({ 283 | owner: owner, 284 | repo: repo, 285 | title: `${advisory.id}: ${advisory.title}`, 286 | body: body, 287 | }); 288 | core.info( 289 | `Created an issue for ${advisory.id}: ${issue.data.html_url}`, 290 | ); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/templates.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Naive way to bundle the templates in order to skip any build/watch pre-processing steps. 3 | */ 4 | 5 | import * as interfaces from './interfaces'; 6 | 7 | export interface ReportWarning { 8 | advisory?: interfaces.Advisory; 9 | package: interfaces.Package; 10 | } 11 | 12 | export const REPORT = ` 13 | {% if vulnerabilities.length > 0 %} 14 | ## Vulnerabilities 15 | 16 | {% for v in vulnerabilities %} 17 | ### [{{ v.advisory.id }}](https://rustsec.org/advisories/{{ v.advisory.id }}.html) 18 | 19 | > {{ v.advisory.title }} 20 | 21 | | Details | | 22 | | ------------------- | ---------------------------------------------- | 23 | {% if v.advisory.informational %} 24 | | Status | {{ v.advisory.informational }} | 25 | {% endif %} 26 | | Package | \`{{ v.package.name }}\` | 27 | | Version | \`{{ v.package.version }}\` | 28 | {% if v.advisory.url %} 29 | | URL | [{{ v.advisory.url }}]({{ v.advisory.url }}) | 30 | {% endif %} 31 | | Date | {{ v.advisory.date }} | 32 | {% if v.versions.patched.length > 0 %} 33 | | Patched versions | \`{{ v.versions.patched | safe }}\` | 34 | {% endif %} 35 | {% if v.versions.unaffected.length > 0 %} 36 | | Unaffected versions | \`{{ v.versions.unaffected | safe }}\` | 37 | {% endif %} 38 | 39 | {{ v.advisory.description }} 40 | {% endfor %} 41 | {% endif %} 42 | 43 | {% if warnings.length > 0 %} 44 | ## Warnings 45 | 46 | {% for w in warnings %} 47 | {% if w.advisory %} 48 | ### [{{ w.advisory.id }}](https://rustsec.org/advisories/{{ w.advisory.id }}.html) 49 | 50 | > {{ w.advisory.title }} 51 | 52 | | Details | | 53 | | ------------------- | ---------------------------------------------- | 54 | {% if w.advisory.informational %} 55 | | Status | {{ w.advisory.informational }} | 56 | {% endif %} 57 | | Package | \`{{ w.package.name }}\` | 58 | | Version | \`{{ w.package.version | safe }}\` | 59 | {% if w.advisory.url %} 60 | | URL | [{{ w.advisory.url }}]({{ w.advisory.url }}) | 61 | {% endif %} 62 | | Date | {{ w.advisory.date }} | 63 | {% if w.versions.patched.length > 0 %} 64 | | Patched versions | \`{{ w.versions.patched | safe }}\` | 65 | {% endif %} 66 | {% if w.versions.unaffected.length > 0 %} 67 | | Unaffected versions | \`{{ w.versions.unaffected | safe }}\` | 68 | {% endif %} 69 | 70 | {{ w.advisory.description }} 71 | {% else %} 72 | ### Crate \`{{ w.package.name }}\` is yanked 73 | 74 | No extra details provided. 75 | 76 | {% endif %} 77 | {% endfor %} 78 | {% endif %} 79 | `; 80 | 81 | export const VULNERABILITY_ISSUE = ` 82 | > {{ vulnerability.advisory.title }} 83 | 84 | | Details | | 85 | | ------------------- | ---------------------------------------------- | 86 | {% if vulnerability.advisory.informational %} 87 | | Status | {{ vulnerability.advisory.informational }} | 88 | {% endif %} 89 | | Package | \`{{ vulnerability.package.name }}\` | 90 | | Version | \`{{ vulnerability.package.version | safe }}\` | 91 | {% if vulnerability.advisory.url %} 92 | | URL | [{{ vulnerability.advisory.url }}]({{ vulnerability.advisory.url }}) | 93 | {% endif %} 94 | | Date | {{ vulnerability.advisory.date }} | 95 | {% if vulnerability.versions.patched.length > 0 %} 96 | | Patched versions | \`{{ vulnerability.versions.patched | safe }}\` | 97 | {% endif %} 98 | {% if vulnerability.versions.unaffected.length > 0 %} 99 | | Unaffected versions | \`{{ vulnerability.versions.unaffected | safe }}\` | 100 | {% endif %} 101 | 102 | {{ vulnerability.advisory.description }} 103 | 104 | See [advisory page](https://rustsec.org/advisories/{{ vulnerability.advisory.id }}.html) for additional details. 105 | `; 106 | 107 | export const WARNING_ISSUE = ` 108 | > {{ advisory.title }} 109 | 110 | | Details | | 111 | | ------------------- | ---------------------------------------------- | 112 | {% if advisory.informational %} 113 | | Status | {{ advisory.informational }} | 114 | {% endif %} 115 | | Package | \`{{ warning.package.name }}\` | 116 | | Version | \`{{ warning.package.version | safe }}\` | 117 | | URL | [{{ advisory.url }}]({{ advisory.url }}) | 118 | | Date | {{ advisory.date }} | 119 | 120 | {{ advisory.description }} 121 | 122 | See [advisory page](https://rustsec.org/advisories/{{ advisory.id }}.html) for additional details. 123 | `; 124 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "newLine": "LF", 10 | "noEmitOnError": true, 11 | "noErrorTruncation": true, 12 | "noFallthroughCasesInSwitch": true, 13 | // TODO: enabling it breaks the `@actions/github` package somehow 14 | "noImplicitAny": false, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "outDir": "dist", 20 | "pretty": true, 21 | "removeComments": true, 22 | "resolveJsonModule": true, 23 | "rootDir": "src", 24 | "strict": true, 25 | "suppressImplicitAnyIndexErrors": false, 26 | "target": "es2018" 27 | }, 28 | "include": [ 29 | "src" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------