Ken Muse

Implementing Processes for GHAS using GitHub Probot


GitHub Advanced Security (GHAS) is a fascinating topic. There’s a lot that it provides out of the box to help teams adopt a shift-left security approach. But what do you do when its processes and practices don’t quite fit your team’s approach? In this post, we’ll look at how to use GitHub Probot to implement your own approaches.

GHAS provides some very opinionated and open-ended approaches. It relies heavily on collaboration and trust, two best practices for highly agile and mature teams. Unfortunately, this approach isn’t always desirable for all teams and may even conflict with corporate practices. In fact, there’s a common challenge for many organizations with this approach: GHAS allows developers to close and disposition security issues without fixing them. For many organizations, they want a more structured approach with approvals and reviews before closing a discovered issue. This is especially true when developers don’t receive regular security training to understand the risks (especially since GHAS has a low rate of false positives). This is a common practice in many organizations, and it is often a security requirement for meeting their compliance goals.

The desired approach is simple and similar to handling a pull request. When a team member wants to close an open security alert, they want to ensure that one or more security specialists have reviewed the alert and agree that it’s a false positive. Only one of those specialists can close the alert, and they must provide a reason for closing it that explains why it poses no actual risks. While this approach is not yet native to GitHub, it is possible to implement and enforce it using GitHub Webhooks and REST APIs.

To understand how this works, let’s quickly cover a few of the basics.

GitHub Wehooks

The first aspect to solving this is to understand GitHub Webhooks. Webhooks are a way for GitHub to notify you when something happens in a repository (or organization). When a support event occurs, GitHub sends an HTTPS POST payload to the webhook’s configured URL. The URL must be publicly accessible so that GitHub can reach it. Beyond that, it’s simply a web application that receives a POSTed payload and responds with a 2xx status to acknowledge receiving the message. To secure the notification, GitHub takes a few precautions:

  • The message is always sent from one of the blocks addresses specified under the hooks element that is returned from https://api.github.com/meta.
  • The message is defaults to HTTPS to ensure it is encrypted.
  • GitHub can check that the HTTPS certificate is valid and issued by a trusted Certificate Authority (CA).
  • GitHub signs the message using HMAC/SHA-256 with a user-provided secret to ensure that the message was not modified.
  • Messages are limited to 25MB to prevent abuse.

That means to properly receive a webhook, you want to use a public HTTPS endpoint and the code should always verify the signature. For extra security, the infrastructure supporting the webhook can restrict the accepted IP addresses. The webhook has one other requirement: it must respond within 10 seconds, or GitHub will consider the delivery a failure. It won’t attempt redelivery, so it’s important to respond quickly.

The GitHub App

Webhooks allow us to listen for events. To react to those events and take action, we need a credential for interacting with the GitHub API. The permissions should be the minimum required to call the necessary endpoints. While this can be done with a personal access token (PAT), it becomes tied to an individual user. If their account is closed or locked, it stops working. Additionally, it’s a long-lived credential and less than ideal for production web components.

A GitHub App provides some important features:

  • It acts as a “bot” credential, allowing it to interact with the API using its own identity.
  • It can be installed on multiple repositories (or organizations) a given a restricted set of permissions.
  • It can optional be associated with a webhook, restricting the messages sent to only those that match the granted permissions.
  • It has a higher rate limit than a PAT, making it more suitable for high-traffic services.

In short, it combines webhooks and an identity. To use the identity, it has to sign a request to receive a short-lived token that it can use to interact with the GitHub REST APIs. Each time it reacts to a webhook, it should request a fresh token that is scoped to the minimum permissions it needs and to the repository that triggered the event.

The identity aspect has another benefit outside of this context. You can use a GitHub App to create short-lived credentials for accessing other repositories as part of a GitHub Actions workflow. My colleague Josh Johanning has and excellent post on this topic if you want to know more.

And then there’s Probot

That’s a lot of requirements if you want to meet all of the best practices for a secure, reliable webhook that can interact with the GitHub API. Fortunately, there’s a project that does all of this for you: Probot. So what is Probot? Essentially, Probot is a JavaScript/TypeScript framework for building GitHub Apps. It provides an opinionated way to extend GitHub’s functionality using NodeJS. As part of that, it handles most of the best practices for webhooks and token-based API calls for you automatically. It even provides a short tutorial to get you started quickly.

At its most basic, in consists of a single file that exports a configured app instance. That instance registers for specific events and actions from the webhook. If you’re using TypeScript, it’s strong-type to avoid mistakes and typos. Each event receives a context object with a configured octokit instance that can be used to make strongly-types calls to the REST APIs. When that is used, Probot automatically requests an appropriate token and uses that to authenticate the request.

As a basic example, consider a situation where you need to know when issues are created. You would listen for an issues event with the action type created. If I wanted to just log the message, the code would look likes this:

1export default (app) => {
2    app.on('issues.created', async (context) => {
3        context.log.info(`Issue #${context.payload.issue.number} was created!`);
4    });
5};

Probot automatically handled receiving and validation the received message, then delegated it to the appropriate handlers. If I want to use the REST API to comment on the issue, I can do that too:

 1export default (app) => {
 2    app.on('issues.created', async (context) => {
 3        context.log.info(`Issue #${context.payload.issue.number} was created!`);
 4        // Create a strongly typed comment
 5        const context = github.context;
 6        const owner = context.payload.sender.login;
 7        const repo = context.payload.repository.name;
 8        const issue_number = context.payload.issue.number;
 9        const body = 'Thanks for opening this issue!';
10        const details = {
11            owner,
12            repo,
13            issue_number,
14            body
15        };
16        // Use octokit to call the issues REST APIs
17        context.octokit.issues.createComment(details);
18    });
19};

Noticed that its using the Octokit createComment method to send the request. If you’re using TypeScript, then the elements are strong-typed, providing you validation. You can also see that the process of authenticating as an App and requesting a token is completely hidden from you. This is the power of Probot.

There’s one other helpful feature. When building a new App and running in development mode, Probot provides support for GitHub App Manifest flow. This is a process in which a form is submitted which describes the App and the permissions it needs. GitHub then creates and registers the App for you. Once you confirm that registration, GitHub sends a message back to Probot that contains the complete configuration required by Probot, including all of the secrets and registration details. Instead of defining the manually and hosting a public service, Probot uses a smee.io proxy to expose the locally running App to the public internet, start the manifest flow, and then automatically configure itself.

Putting it all together

During one of my FastTracks after joining GitHub, I had a customer that was facing the exact issue I described above. They wanted to implement a process that required a security specialist to review and approve the closure of a security alert. To help them, I quickly build a sample GitHub App that demonstrated these approaches to listen and respond to:

  • code_scanning_alert.closed_by_user
  • dependabot_alert.dismissed
  • secret_scanning_alert.resolved

For each event received, it used context.octokit.teams.getMembershipForUserInOrg to determine if the person responsible for the event was a member of a specific team (using the team_slug for the team). If the user was not a member of the team, context.octokit.{FEATURE}.updateAlert was used to change the alert’s state back to open. This ensured that only the specified team members could close the alert. As an exercise for the user, the code could be extended to create an issue or send an external notification. If more complex processing was needed, it could queue the message for further handling (to stay within the 10 second limit).

Since then, the code for that has moved to our Advanced Security organization as a generally available sample. Feel free to review it an see a complete project (with some testing)! It’s a great way to see how to implement a process that is not natively supported by GitHub. Of course, feel free to use that sample to understand how to implement other business processes that interact with GitHub. You aren’t limited to just GHAS … it can be anything!

As a final note, I want to point out that this is a sample and not a supported product (even though its found a surprising number of users). It’s a great way to see how to implement a process that is not natively supported by GitHub. I’ve linked to the v1 version of the sample. It provides an easy to understand structure and implementation of the project for anyone interested in how to implement these solutions.

After a couple of years of work, there’s also a v2 version that is in the works. It will be focused on easy of use and deployment for those that just want to see how it works in their own environment without needing to understand the code. It will also demonstrate some approaches for implementing more complex solutions that are portable and reusable. Stay tuned for that!