Almost nothing is more ubiquitous in applications than logging libraries. No matter which type of application - hastily thrown-together prototypes, decades-old enterprise monoliths, newly built event-driven serverless apps - there is always the need to log. Even in non-production-grade applications where standard observability patterns such as monitoring and alerting might not be applied - logging is usually always used. Log4j has been without a doubt one of the pioneers in the logging framework sphere and continues to be widely used today making the recent discovery of a zero day exploit in Log4j a good time to highlight how important it is to keep all dependencies of your applications up-to-date at all times.
In this blog post we will show how to set up an automatic dependency update process that additionally detects security risks like the previously mentioned Log4j vulnerability. For this, we will make use of GitHub Actions’ built-in Dependabot feature.
Why GitHub Actions you may ask? There are several other options out there, e.g. platforms such as Snyk, open-source tooling renovate bot and of course GitLab’s Dependency Scanner feature available in the Ultimate edition. But even though we are known to be big fans of GitLab CI/CD - we don’t discriminate against any CI/CD system! And usually the choice between GitHub and GitLab is not made based solely on their CI/CD offering. In the case of dependency updates, the integration inside GitHub is just so awesomely flawless, plus it is included for free in each plan that we have chosen for this blog post. Additionally, we see more and more of our clients adopting or considering adopting GitHub Actions, and I think one of the reasons for this is the seamless integration with the whole software development life cycle compared to external build servers or SaaS offerings. And while GitHub Actions might not (yet) be a fully-fledged build server, I do believe it is a sensible choice for many organizations.
To showcase the automatic dependency process using GitHub Action’s Dependabot feature, we have created a simple Java project with the Log4J dependency set to the last version containing the vulnerability. We expect our automated process to a) update the library to the version containing the fix, and b) alert us that the version we are currently using poses a security risk. Note that the project uses Maven instead of Gradle because certain Dependabot features are not fully supported for Gradle at the time of this writing.
Configure Automatic Updates
In order to enable the auto-update functionality for a GitHub repository, all you need to do is create a simple configuration file inside the
.github folder called
dependabot.yml. The mandatory arguments are:
package-ecosystem: The package manager being used like npm, Maven or pip. All major package managers are supported. It is also possible to define several different package managers, e.g. if your code base contains both your Terraform infrastructure code and your application code, you can simply create two different entries in the configuration file, one for each ecosystem.
directory: The directory which contains the files that define the dependencies e.g. the
requirements.txt. In many cases this is simply the root directory.
schedule.interval: How often to check for dependency updates. Possible options are daily, weekly or monthly. What makes sense depends on the workflow you want to use and how to integrate it into you and your team’s daily work. More about this in a later section.
There are several other configuration options available, e.g. which strategy to apply when updating a version, the assignees of the created pull requests or finer-grained control over which dependencies to include and exclude. Make sure to check out the full list before getting started.
In the picture below you see the minimal file for our sample Maven project. The updates are performed on a daily basis at a randomly chosen time (the default), and the file containing the dependencies is located in the root directory.
That’s it! Shortly afterwards, pull requests that contain the updated dependency are automatically created. A dedicated pull request is created for each dependency as shown below.
The level of detail shown on the individual pull request depends on the library itself and how much information it provides. In this case - unfortunately not a lot. We will show an example with more details further below.
Of course, this feature really shines when combined with a GitHub Actions workflow. For every pull request created by Dependabot, the complete workflow is run and if all checks are executed and the results displayed. Thus, you can merge the update knowing that everything still works as expected or know immediately when an update would break your application.
We have now successfully finished the first part of our goal - to update the library to the version containing the fix. However, what if this pull request was created on a Friday night and no one looked at it before Monday morning? The breach would remain open for days or even weeks if the update only runs monthly. Thus, what is still missing is that we get alerted when the version we are currently using poses a security risk.
Configure Alerts for Vulnerabilities
In order to receive alerts about vulnerabilities, the repository’s security & analysis settings need to be set accordingly. The Dependabot alerts feature needs to be enabled. Optionally, you can additionally enable _Dependabot security updates _which will cause Dependabot to create pull requests for newly detected vulnerabilities outside of your regular update interval. One of the additional options you have is to control who sees these alerts. See the full documentation for details.
To ease the setup and ensure that alerts are enabled for all repositories inside an organization, the organization’s security & analysis settings provide additional options to enable alerts for all repositories and to enable them for all newly created repositories as depicted below.
After the alerts have been enabled, we see a big yellow banner on our repository’s main page. Additionally, depending on our notification settings, an email is also sent informing us about the vulnerability.
In the UI, the See Dependabot alerts button takes us to the Security tab that displays the vulnerabilities as shown in the following picture.
By clicking on an alert, the details of the vulnerability and the remediation are shown as depicted below.
Now we have also completed the part where we get alerted about the newly detected vulnerability. Note that both parts don’t depend on each other and can be implemented without the other. It is possible to only update dependencies on a regular basis without being automatically alerted about any vulnerabilities by just adding the
dependabot.yml configuration file. At the same time, it is also possible to just receive the alerts about new vulnerabilities and to have only those updated automatically by enabling the respective _Dependabot _features in the repository’s settings. No additional configuration is required for that.
However, we believe that a combined approach makes the most sense. Regular updates ensure that you stay close to the most recent version. Updating in small increments is usually much simpler, faster and less risky than performing an update that spans multiple versions. Thus, in case of an urgent bug fix, e.g. because of a vulnerability, you are able to act quickly and confidently because the change is likely very small and updating dependencies is a routine task. Something you want when the pressure is high. Security alerts and automatically triggered updates outside the regular interval ensure that you don’t miss anything important and are able to act immediately without having to wait for the next run of the update procedure.
Which brings us to the last part… How often to run the update procedure and how to deal with the created pull requests.
Handling the Pull Requests
As is often the case, setting up the tooling is one thing; setting up a working process and incorporating it into the regular workflow of you and your team is another. In this last section we want to share some recommendations and findings.
When it comes to handling the individual pull requests, we strongly recommend not just blindly merging them. Staying on top when it comes to security might be the main driver for setting up automatic dependency updates. But there are additional reasons.
- Support: Continue to receive support and be able to effectively use the library e.g. by using a version with up-to-date documentation, a version that still receives bug fixes, a version where it is possible to get enough relevant support on Stack Overflow.
- Compatibility: Remain compatible with the underlying platform, the newest compiler version, etc…
- New Features: Be able to make use of new features that might simplify or improve the existing code base, might increase developer productivity or might lead to better performance.
This is why we suggest always taking some time to review the release notes and take note of any new changes that might be relevant. Dependabot offers quite a convenient way to keep track of those changes without having to manually subscribe to individual projects’ news feeds. As shown in the example below for an update of Spring Boot, the information included in the Dependabot pull request is quite extensive.
However, this example also highlights one of the challenges we have encountered when using automated dependency update processes. There are so many changes that it is often not a very small task to review the dependency update. Just reading through the complete release notes takes a while. In the case of Spring Boot, where an update often includes updates of several included libraries, reading through all those transitive changes takes additional time. And this does not even consider the cases where the change requires a code change - be it an immediate one because of a breaking change or one for later e.g. a deprecation or simplification. We have seen a couple of different ways how teams deal with this and each one of course comes with its own pros and cons. The two main approaches we have encountered are to continuously receive updates or to have a recurring task that handles the updates in larger batches.
What is common for all approaches is that very good (functional) test coverage and full automation is required. Having an automated update process naturally leads to many more updates than having a manual process, so it is key that all changes can be verified quickly.
This is even more important for the continuously receiving updates approach. This approach leads to constant unplanned work. Without good automation and observability in place, the required manual work would soon become too much to handle. It also requires a process and discipline on how to deal with required changes that are too large to handle ad-hoc e.g. create a ticket to complete in the next sprint. Additionally, this approach might increase the risk that new features go undetected, or obsolete workarounds are not removed because change logs are not read thoroughly anymore since updating dependencies is no longer a conscious task. Another drawback is that unnecessary work might be performed. New versions are picked up almost immediately, and you might be one of the first to encounter a newly introduced bug or have to deal with some accidentally introduced breaking change.
However, the obvious benefit of this approach is of course that dependencies are always up-to-date, and it is always possible to act fast in case of a vulnerability. This is why we usually prefer this approach over any other.
In this blog post we have shown how to set up automatic dependency updates and alerts about vulnerabilities with GitHub Actions’ Dependabot feature. Staying ahead when it comes to security has always been relevant, but it is getting ever more important. If we can support you in any way in making your processes and applications more secure, please don’t hesitate to reach out!
Except maybe against AWS’ own offerings… but 🤫 … don’t tell them. ↩