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
}
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'
}
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
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.
Clicking the 'Details' link leads us to a detailed overview of the workflow execution.
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
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"
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);
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
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
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: ./
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
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
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 😛)
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() }}"
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
That should do the trick and the workflow should succeed.
The entry 'Owasp Overview Report' contains a zip, which includes the generated PDF.
This was a brief overview about GitHub Actions and Workflows. Glad to receive some feedback :D Thanks for reading!
Top comments (0)