GitHub Action for creating a custom OWASP dependency check report
Tags: security, github, javascript, tutorial
Published: 2021-01-16 21:40:15

[GitHub Actions](https://github.com/features/actions) can be considered as the building blocks to create automated workflows in GitHub, which definitely is a considerable option if you use GitHub as your code repository.

In this post we're going to have a look into GitHub Actions and Workflows by defining a workflow and make use of readily available actions from GitHub's marketplace, as well as have a custom action invoked.

## The example project
We're going to have a look on a few things around the java project which we'll use as the subject of dependency checking. It is available under https://github.com/perpk/a-vulnerable-project. 

It's best to fork it in order to follow along with the following sections of this guide.

The project uses [Gradle](https://gradle.org/) as the build tool. It's build file contains a dependency to an older version of the [Spring Framework](https://spring.io/), which happens to have a few vulnerabilities.

Let's have a look at the project's buildfile.

```groovy
plugins {
    id 'java'
    id 'org.owasp.dependencycheck' version '6.0.5'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencyCheck {
    format = "JSON"
    failBuildOnCVSS = 7
}
```
The `plugins` block contains the plugin we are going to use to perform the dependencies check on our project (_Details on gradle plugins can be found [here](https://plugins.gradle.org/) and the documentation on the plugin is available [here](https://jeremylong.github.io/DependencyCheck/dependency-check-gradle/index.html)_). 

The `dependencyCheck` block contains some configuration for the plugin. Here we only want to set the output format which we'll later parse from in our GitHub action and when we want the build to fail. The trigger for this is whether there are any high and above (_critical_) vulnerabilities detected. The score according to [OWASP](https://owasp.org/www-community/OWASP_Risk_Rating_Methodology) technically defines the severity of a vulnerability.

Now you can create a branch and edit the `build.gradle` file by adding a `dependencies` block at the bottom like this one

```groovy
dependencies {
    runtime group: 'org.springframework', name: 'spring-core', version: '2.0'
}
```

At this point you can give it a shot and run the `dependencyCheckAnalyze` task locally via the following command on the root directory of the project.

```bash
./gradlew dependencyCheckAnalyze
```
The build will fail since there are vulnerabilities which have scores equal or above the value we set for `failBuildOnCVSS` in our `build.gradle` file.

Let's check whether our GitHub Workflow does the same at this point. Push your newly created branch and create a pull request.

Right after the pull request is created the workflow is started and after a few moments the verdict for the check arrives, which as expected failed.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/xilb1z61cknjzxvqx34i.png)

Clicking the 'Details' link leads us to a detailed overview of the workflow execution.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qi2sqp6vdk56nq6v3bad.png)

Expanding the step with the error should have the same error in the displayed log as the one you encountered when you ran it locally.

## Dissecting the workflow
Now the highlight of the example project, the workflow file. It's a yaml file which can be found under `.github/workflows/dependencyCheckReport.yml`. Here's the content and some details below.

```yml
name: Java CI with Gradle

on:
  pull_request:
    branches: [ main ]

jobs:
  depCheck:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - uses: eskatos/gradle-command-action@v1
        with:
          arguments: dependencyCheckAnalyze
```

Pretty concise, isn't it? Let's inspect it from top to bottom! 
* The 1st block declares when this workflow shall be triggered. Here it is on each pull request which targets the main branch. That makes sense, since we don't want to have security issues on our main branch.
* The `jobs` block contains all job declarations for the current workflow. For the time being we have just one job, which performs the execution in its entirety. 
* A job has one to several steps. In our example we start with a step which uses an existing action from the [GitHub Marketplace](https://github.com/marketplace/actions/checkout). The particular action checks out the code from our project from its repository. 
* Following the checkout another readily available action is used to setup java. This action also accepts a parameter with the particular version of java to set up.
* At the end, we use another action from the GitHub's marketplace which helps us run Gradle commands. In our scenario, we need to run `dependencyCheckAnalyze` to trigger the OWASP analysis on the project's dependencies.

## A Custom Action
For now we have the output of the dependency analysis dumped on stdout. What if we'd like to have a concise report containing all vulnerable dependencies alongside with their vulnerabilities and their severity in a printable format, at best also sorted by severity in descending order?

If that's what we want, chances are we need to implement something by ourselves and have it called in our workflow.
Here you can also fork this repo https://github.com/perpk/owasp-report-custom-render
where such an action is already implemented.

### Anatomy of the Action
The centerpiece of the action is the `action.yml` file, available at the action's project root directory.

```yml
name: "Owasp Report Custom Renderer"
description: "Render OWASP Report with few informations as an overview in pdf"
inputs:
  owasp-json-report:
    description: "The owasp report with the dependencies and their vulnerabilities in the JSON format"
    required: true
runs:
  using: "node12"
  main: "index.js"
```
Following the name and a general description the inputs of the action are defined. The inputs are named parameters which as we're going to see next are used in the action's source code to retrieve parameters passed from within a workflow.

The `runs` block defines the runner for our action. Here we have a [Node.JS](https://nodejs.org/) action. The `main` keyword defines the file to execute.

We'll now have a glance at `index.js` which implements our entrypoint (so to speak).

```JS
const core = require("@actions/core");
// ... some more imports

const work = async (owaspReportJsonFile, dumpHtmlToFS = false) => {
  try {
    const owaspReportData = await owaspJsonReportReader(owaspReportJsonFile);
    const html = createHtmlOverview(owaspReportData, dumpHtmlToFS);
    writePdfReport(html);
  } catch (e) {
    core.setFailed(e);
  }
};

work(core.getInput("owasp-json-report"), true);
```

There's an import of the package [`@actions/core`](https://www.npmjs.com/package/@actions/core) which provides core functions for actions. In the code above, it's used for error handling and to read an input, as visible in the last line. The input we want to read here is the json report as generated by the `dependencyCheckAnalyze` Gradle task which is run by the workflow. Our action expects the json report to be available at the same directory as index.js is.

The action itself will first create the report in HTML and then finally transform it to PDF. There are libraries available to directly generate PDF but I find it more convenient to create a reusable, intermediate format as HTML. I also find it easier to do it this way rather than dealing with the API of a PDF library.

### Invoking the Action in the Workflow
We now will change our workflow by invoking our action, pass a parameter to it and access its result. 

First we will need to have the json report generated by `dependencyCheckAnalyze` at hand, since we want to pass it as a parameter to our action. In order to make it available for the next job in our workflow, we need to have it in the GitHub provided storage. To do so we'll use the action [`actions/upload-artifact`](https://github.com/marketplace/actions/upload-a-build-artifact) from GitHub's marketplace.

```yml
      - name: Backup JSON Report
        uses: actions/upload-artifact@v2
        with:
          name: dependency-check-report.json
          path: ./build/reports/dependency-check-report.json
```

Adding this snippet to the bottom of our workflow file will invoke the `upload-artifact` action which will take the report from the specified path and store it with the given name.

Then, we need to define another job which shall run after the 1st one has completed. It is necessary to wait since we need the json report to proceed with transforming it to PDF.

```yml
  owasp_report:
    needs: [depCheck]
    runs-on: ubuntu-20.04
    name: Create a report with an overview of the vulnerabilities per dependency
```

Since our action isn't available in the Marketplace, we'll need to check it out from its repository in the first step of the newly created job. As a second step after the checkout, we need to fetch the previously uploaded json report. The path defines where the file shall be downloaded to. In our case it's sufficient to do that in the current directory, which happens to also be the directory where the action's sources are checked out at.

```yml
steps:
      - uses: actions/checkout@v2
        with:
          repository: perpk/owasp-report-custom-render
      - uses: actions/download-artifact@v2
        with:
          name: dependency-check-report.json
          path: ./
```

We now can invoke the actual action. This happens via the uses keyword. It must have a reference to the directory where the `action.yml` file is located. In our case it's the current directory.

```yml
      - name: Run Report Creation
        uses: ./
        with:
          owasp-json-report: dependency-check-report.json
```

The last thing to do is to get the PDF report as generated by the action and upload it and thus have it available for further distribution.

```yml
      - name: Upload overview report
        uses: actions/upload-artifact@v2
        with:
          name: Owasp Overview Report
          path: owasp-report-overview.pdf
```

We now can commit/push our changes to the workflow file, create another pull request and lay back and enjoy the miracle of automation! (slightly exaggerated :stuck_out_tongue:)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cr49g1uk7d0zdmuf97hp.png)

Oops! Since we have a condition to fail the build based on the vulnerability score, our report generation job didn't execute at all.

The solution to that is rather simple. Job execution can be combined with conditions. In our case we'd like to execute the report generation no matter what. To do so, we'll jam in another line right below the `needs` keyword in our workflow.

```yml
  owasp_report:
    needs: [depCheck]
    if: "${{ always() }}"
```

Since the `dependencyCheckAnalyze` step is failing, all subsequent steps aren't executed. Therefore we'll also add the condition to the first step following the failing one.

```yml
      - name: Backup JSON Report
        if: "${{ always() }}"
        uses: actions/upload-artifact@v2
        with:
          name: dependency-check-report.json
          path: ./build/reports/dependency-check-report.json
```

That should do the trick and the workflow should succeed.
The entry 'Owasp Overview Report' contains a zip, which includes the generated PDF. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/91j8ljnfq1epdfrarzi4.png)

This was a brief overview about GitHub Actions and Workflows. Glad to receive some feedback :D Thanks for reading!