Securing GitHub CodeSpaces with Trivy

Securing GitHub CodeSpaces with Trivy

Securing GitHub CodeSpaces with Trivy

In this article I show how to create a container for GitHub CodeSpaces for some Azure + Terraform work. Usually, azterraform from Microsoft is my go-to, but in this instance I needed a PowerShell base, and the addition of the GitHub CLI client.

Obviously, it has to be built in CI, and let’s add some security love with Trivy too!

You can re-use this same process to make your own CodeSpaces container (or any container, for that matter), with the tools that work for your use cases.

Updating the GitHub PR with Trivy results

Here’s the CI pipeline in a nutshell:

  • Build the docker image & scan it for security smells.
  • Publish the report summary table as a comment on the PR.
  • Create a link to the full Trivy report on the Actions step summary page.

Here’s some pictures from the end state:

alt text The Trivy report table posted as a PR comment - and a link to the full report alt text We're going to use the Actions 'summary' page for the full report alt text A taster from the full output

Build the workflow

The full workflow is available in /.github/workflows/docker-pr-build-and-scan.yml.

Starting at the top:

1
2
3
4
5
6
7
8
name: Docker PR Build and Scan
on :
  pull_request :
    branches :
    - main
  permissions:
    contents: read
    pull-requests: write

With the fine-grained access model, we need to make sure the permissions block allows write access to the PR, so we can add comments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
jobs :
  build-and-scan:
    runs-on: ubuntu-latest

    steps :
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses : docker/setup-buildx-action@v2

      - name: Build Docker image
        run: |
          docker build -t pr-az-pwsh-terraform: latest

      - name: Scan Docker image with Trivy and generate summary
        uses: aquasecurity/[email protected]
        with:
          image-ref : 'pr-az-pwsh-terraform: latest'
          format: 'table'
          output : 'trivy-results . txt '
          severity: 'CRITICAL, HIGH, MEDIUM'

The build process is pretty standard - in the final step we’re using Aqua Security’s GitHub Action for Trivy to run the scan.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- name: Post Trivy scan results to PR
  uses: actions/github-script@v6
  if: github.event_name == 'pull_request'
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
      const fs = require('fs');
      const fullResults = fs.readFileSync('trivy-results.txt', 'utf8');
      
      let summarySection = '';
      const summaryMatch = fullResults.match(/Report Summary\n\n(.*?)\nLegend:/s);
      
      if (summaryMatch && summaryMatch[1]) {
        summarySection = summaryMatch[1].trim();
      } else {
        summarySection = "No vulnerability summary found. Please check the full report in the Actions tab.";
      }
      
      const header = '## Trivy Security Scan Summary\n\n';
      const footer = `\n\n---\n\n*For detailed results, please [view the actions output](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}).*`;
      
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: header + '```\n' + summarySection + '\n```' + footer
      });

This will take the Trivy report and extract just the report table at the top, and then add this to the PR as a comment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
- name: Publish Trivy to action log
  run: |
    if [[ -s trivy-results.txt ]]; then
      {
        echo "### Trivy Scan Results"
        echo "<details><summary>Click to expand</summary>"
        echo ""
        echo '```terraform'
        cat trivy-results.txt
        echo '```'
        echo "</details>"
      } >> $GITHUB_STEP_SUMMARY
    fi

Finally - we use the “well known” variable GITHUB_STEP_SUMMARY to post the contents to the Actions log.

That’s the end of the PR flow.

Publishing to Docker Hub

A separate action publishes the container to Docker Hub once the PR is merged.

Here it is: kewalaka/az-pwsh-terraform - Docker Image | Docker Hub

Use in CodeSpaces

Here’s an example of how you can use this in your own repositories, including the config to switch the default shell to PowerShell (place in .devcontainer/devcontainer.json)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "image": "docker. io/kewalaka/az-pwsh-terraform:latest",
  "runArgs": [
    " -- cap-add=SYS_PTRACE",
    " -- security-opt",
    "seccomp=unconfined",
    " -- init",
    " __ network=host"
  ],
  "mounts": [
    "source=/var/run/docker.sock, target=/var/run/docker.sock, type=bind"
  ],
  "customizations": {
    "vscode": {
      "extensions": [
        "azapi-vscode.azapi",
        "EditorConfig. EditorConfig",
        "hashicorp. terraform",
        "davidanson. vscode-markdownlint"
      ],
      "settings": {
        "terminal. integrated. defaultProfile. linux": "pwsh"
      }
    }
  }
}

Here is an example: terraform-azure-starter-template/.devcontainer/devcontainer.json

Call to action

Perhaps try this container out in your own CodeSpaces?

It is very customised to my use case hence you may not agree with my choice of tooling - but you can always simply re-use the above with just a change to the Dockerfile, then have one that works just for you!

Where next?

In my first iteration, I built the container on the PR, and then I re-build it when pushing to Docker Hub, and when tagging.

This is sub-optimal, because what I end up with after the PR might not be the same as what gets built in the deploy or tagging stage.

In the next article, I’ll illustrate one way to fix this - by using GHCR as a staging ground before promoting it to Docker Hub.

This post is licensed under CC BY 4.0 by the author.