DEV Community

loading...
Cover image for GitHub Action for creating a custom OWASP dependency check report

GitHub Action for creating a custom OWASP dependency check report

perpk profile image Kostas Perperidis ・7 min read

GitHub 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 as the build tool. It's build file contains a dependency to an older version of the Spring Framework, which happens to have a few vulnerabilities.

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

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
}
Enter fullscreen mode Exit fullscreen mode

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 and the documentation on the plugin is available here).

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 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

dependencies {
    runtime group: 'org.springframework', name: 'spring-core', version: '2.0'
}
Enter fullscreen mode Exit fullscreen mode

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.

./gradlew dependencyCheckAnalyze
Enter fullscreen mode Exit fullscreen mode

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

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

Alt Text

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.

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
Enter fullscreen mode Exit fullscreen mode

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. 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.

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"
Enter fullscreen mode Exit fullscreen mode

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 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).

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);
Enter fullscreen mode Exit fullscreen mode

There's an import of the 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 from GitHub's marketplace.

      - name: Backup JSON Report
        uses: actions/upload-artifact@v2
        with:
          name: dependency-check-report.json
          path: ./build/reports/dependency-check-report.json
Enter fullscreen mode Exit fullscreen mode

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.

  owasp_report:
    needs: [depCheck]
    runs-on: ubuntu-20.04
    name: Create a report with an overview of the vulnerabilities per dependency
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

      - name: Run Report Creation
        uses: ./
        with:
          owasp-json-report: dependency-check-report.json
Enter fullscreen mode Exit fullscreen mode

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.

      - name: Upload overview report
        uses: actions/upload-artifact@v2
        with:
          name: Owasp Overview Report
          path: owasp-report-overview.pdf
Enter fullscreen mode Exit fullscreen mode

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 😛)

Alt Text

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.

  owasp_report:
    needs: [depCheck]
    if: "${{ always() }}"
Enter fullscreen mode Exit fullscreen mode

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.

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

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

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

Discussion (0)

pic
Editor guide