How to leverage GitHub and semantic release to reduce vulnerabilities in your packages

8 min read • 1,535 words

Github defending against NPM vulnerabilities

You most probably know from personal experience that many projects nowadays require a bunch of dependencies to develop, compile or run.

Have you ever noticed though, when you install your project’s dependencies, that npm is also reporting known vulnerabilities in those dependencies?

npm install reporting vulnerabilities

npm install reporting vulnerabilities

Even if your project is only using a handful direct dependencies, chances are that those dependencies also have transitive dependencies on their own, and so on… You see where I’m going with this.

Let’s have a look at the dependencies tree of the demo project

List of dependencies and their vulnerabilities in both package.json and package-lock.json

List of dependencies and their vulnerabilities in both package.json and package-lock.json

While the project itself “only” has eight (8) direct dependencies referenced in its package.json, the whole dependencies tree contains a total of 533 packages.

Keeping track of all those dependencies and their vulnerabilities quickly becomes a tedious if not impossible task to handle manually.

This is where GitHub can help.

Note

All following code snippets can be forked from the demo repository on GitHub.

GitHub

Github recently published a list of best practices to mitigate OWASP vulnerabilities.

Explore how GitHub Advanced Security can help address several of the OWASP Top 10 vulnerabilities

Explore how GitHub Advanced Security can help address several of the OWASP Top 10 vulnerabilities

One of the tips is to leverage Dependabot to alert developers about known vulnerabilities in dependencies and potentially update those dependencies to a patched version.

The first step is to activate Dependabot alerts and security updates. This can be done easily in the repository settings under Code security and analysis as you can see in the screenshot below.

Github’s “Code security and analysis” repository settings with Dependabot alerts & Dependabot security updates enabled

Github’s “Code security and analysis” repository settings with Dependabot alerts & Dependabot security updates enabled

Dependabot alerts

The first setting will alert you when there are known vulnerabilities in dependencies you are using like running npm audit would, but it will also help you to track and manage those vulnerabilities.

The dependabot alerts page showing a single moderate vulnerability

The dependabot alerts page showing a single moderate vulnerability

Dependabot security updates

The second setting will enable dependabot to try and fix the vulnerabilities automatically. If a fixed version of the dependency is available and compatible with your repository, Dependabot will open a pull request to update the affected package.

Dependabot version updates

But why stop there? The third option, Dependabot version updates, will help you create a Dependabot configuration to automatically update all your dependencies as well.

The code below is the most basic Dependabot configuration to update your npm dependencies on a weekly basis and add a user or team as reviewer to all pull requests dependabot would create.

# .github/dependabot.yml  
version: 2  
updates:  
 - package-ecosystem: "npm"  
 directory: "/" # Location of package manifests  
 schedule:  
 interval: "weekly"  
 reviewers:  
 - "<user or team name>"

Version updates can be further customized in many ways following the official dependabot documentation.

Bonus: Keep your CI up to date

While this is not directly related to your dependencies, it might help keep your CI secure too. By adding a few lines to your dependabot.yml, you can also configure it to also keep your Github Actions up to date. You just need to add a second package ecosystem with the following lines:

- package-ecosystem: "github-actions"  
 directory: "/"  
 schedule:  
 interval: "daily"

The demo repository’s dependabot config is set to update both npm packages and Github Actions on a daily basis.

Auto merge Dependabot PR

In projects with a lot of dependencies, the amount of pull request generated by Dependabot can quickly become a burden to review and merge. If you have a test automation to validate your pull requests, this is something you no longer want to do manually.

Important

Before implementing this step, make sure to setup branch protection and a CI (build, test, deploy) you can trust since you will no longer be validating those changes manually.

Make sure that PR need to be approved and your relevant CI workflows are successful before allowing pull requests to be merged

Make sure that PR need to be approved and your relevant CI workflows are successful before allowing pull requests to be merged

Automatically merging Dependabot pull requests can be achieved by creating a workflow leveraging the Github API…

# .github/workflows/dependabot-automation.yml
name: Depandabot automation
on: 
 pull_request_target:    
  types: [opened, synchronize, reopened]
permissions:
  pull-requests: write
  contents: write
jobs:
  dependabot-auto:
    name: '🤖 Dependabot Automation'
    runs-on: ubuntu-latest
    # Only run if PR was opened by dependabot
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v1
          with:
            github-token: '${{ secrets.GITHUB_TOKEN }}'
      - name: Enable auto-merge for Dependabot PRs
        continue-on-error: true
        # Auto merge dependencies with patch or minor updates
        # Major versions still must be merged manually
        if: ${{(steps.metadata.outputs.update-type == 'version-update:semver-patch') || (steps.metadata.outputs.update-type == 'version-update:semver-minor')}}
        run: gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          # GH_TOKEN needed to trigger follow up worlflow: https://github.com/fastify/github-action-merge-dependabot/issues/134
          GITHUB_TOKEN: $

If your branches are configured to require approval before merging, you’ll also need to add the following step to the previous workflow to handle this as well.

- name: Approve a PR
    continue-on-error: true
    run: gh pr review --approve "$PR_URL"
    env:
      PR_URL: ${{github.event.pull_request.html_url}}
      # GH_TOKEN needed to approve PR as code owner on protected branches
      GITHUB_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} 

Alternatively, you can also rely on Github Actions like Dependabot Auto Merge to simplify the workflow similar to the solution implemented in the demo.

# This is the workflow to automatically approve and merge dependabot PRs
#
name: Dependabot automation

on:
  pull_request_target:
    types: [opened, synchronize, reopened]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  dependabot:
    name: '🤖 Dependabot Automation'
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}

    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}

      - uses: ahmadnassri/action-dependabot-auto-merge@v2
        with:
          target: minor
          github-token: ${{ secrets.DEPENDABOT_TOKEN  }}
          command: squash and merge

Avoiding introducing vulnerabilities with new dependencies

The Github team recently published a new action called dependency-review-action that can further help reduce vulnerable dependencies by scanning pull request and blocking the addition of vulnerable dependencies before they can even make it in your code.

name: Check Dependencies

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read

jobs:
  dependency-review:
    name: '⛓️ Dependency Review'
    runs-on: ubuntu-latest

    steps:
      - name: '☁️ Checkout repository'
        uses: actions/checkout@v3

      - name: '🛡️ Dependency Review'
        uses: actions/dependency-review-action@v3

With all this in place, you have already reduced vulnerabilities in your code. But until you create a new release your package is still affected.

This is often a problem for legacy packages or packages that are not actively or regularly maintained. This is not something you want to happen to your package once you move on to your next project!

semantic-release

semantic-release is a tool that helps you automate the release process of your package, including update the version number, generating release notes and changelog and publishing the package to a registry.

Commit format

For semantic release to be able to determine which part of your semantic version to bump, your commits need to follow a commit convention.

In our example we use commitlint with an extension of its default settings to make sure semantic release will be able to use them.

Auto release security fixes

Your also need to configure semantic-release…

Additionally, to select which branch should trigger a release, you might also want to add a few plugins to generate release notes and a changelog, publish your package and generate a GitHub tag.

The following settings will also make sure that the generated changelog as well as the package config with the new version are committed to your repository.

{
 "branches": ["main"],
 "plugins": [
  "@semantic-release/commit-analyzer",
  "@semantic-release/release-notes-generator",
  "@semantic-release/changelog",
  "@semantic-release/npm",
  [
   "@semantic-release/git",
   {
    "assets": ["CHANGELOG.md", "package.json", "package-lock.json"]
   }
  ],
  "@semantic-release/github"
 ]
}

That’s it?

Not quite. The default commit message from dependabot won’t trigger a new release, so there is still something we need to do.

Matt Rathbun wrote a straightforward guide to update your semantic-release configuration to automatically trigger a new patch release for dependencies update.

The configuration consists of 2 changes:

"release": 
  // ...
  "plugins": [
    [
      "@semantic-release/commit-analyzer",
      {
        "preset": "conventionalcommits",
        "releaseRules": [
          {
            "type": "build",
            "scope": "deps",
            "release": "patch"
          }
        ]
      }
    ],
    [
      "@semantic-release/release-notes-generator",
      {
        "preset": "conventionalcommits",
        "presetConfig": {
          "types": [
            {
              "type": "feat",
              "section": "Features"
            },
            {
              "type": "fix",
              "section": "Bug Fixes"
            },
            {
              "type": "build",
              "section": "Dependencies and Other Build Updates",
              "hidden": false
            }
          ]
        }
      }
    ]
    // ...
  ]
}

All that is left to do is to trigger semantic-release with every push on your main branch.

name: 'Semantic release'

on:
  push:
    branches: [main]

jobs:
  # jobs to check your code come here

  release:
    name: '🏷️ Release'
    runs-on: ubuntu-latest

    environment:
      name: production
      url: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.release.outputs.version }}

    outputs:
      version: ${{ steps.release.outputs.version }}

    steps:
      - name: '☁️ Checkout repository'
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: '⚙️ Use Node.js'
        uses: actions/setup-node@v3
        with:
          check-latest: true
          cache: 'npm'

      - name: '⛓️ Install dependencies'
        run: npm ci --omit=optional --audit=false --prefer-offline --progress=false

      - name: '📦 Release'
        id: release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} # Needed to push to GitHub
        run: |
          npx semantic-release
          echo "::set-output name=version::$(npm run env | grep npm_package_version | cut -d '=' -f 2)"n::$(npm run env | grep npm_package_version | cut -d '=' -f 2)"

And your done… Now, every time Dependabot fixes a vulnerability in your dependencies, the CI will issue a new release of your package with a fix.

Conclusion

By combining those two tools, you can reduce the risk of vulnerabilities in your packages without spending a minute of your time… I’d say, that is definitely worth spending the initial effort to do a little bit of configuration.

What do you think?