From b505256cae6b7548281078f318bf3563557a141d Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Wed, 8 Feb 2023 20:27:17 +1100 Subject: [PATCH] security: add trivy scanning workflow (#195) --- .github/workflows/build.yaml | 37 ++++++-- .gitignore | 4 +- images/base/Dockerfile.centos | 4 +- images/base/Dockerfile.ubuntu | 6 +- images/base/containerd-pin | 3 - scripts/scan_images.sh | 163 ++++++++++++++++++++++++++++++++++ 6 files changed, 198 insertions(+), 19 deletions(-) delete mode 100644 images/base/containerd-pin create mode 100755 scripts/scan_images.sh diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e94c956..5cf3386 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,7 +25,7 @@ on: workflow_dispatch: permissions: - actions: none + actions: read checks: none contents: read deployments: none @@ -33,13 +33,13 @@ permissions: packages: none pull-requests: none repository-projects: none - security-events: none + security-events: write statuses: none jobs: # Quick checks, running linters, checking formatting, etc quick: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Cancel previous runs if: github.event_name == 'pull_request' @@ -60,14 +60,14 @@ jobs: run: yarn format:check images: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest-8-cores strategy: matrix: job: - centos - ubuntu fail-fast: false - name: images/${{ matrix.job}} + name: images/${{ matrix.job }} steps: - name: Cancel previous runs if: github.event_name == 'pull_request' @@ -76,11 +76,34 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Install Trivy using install script + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.37.1 + - name: Build ${{ matrix.job }} images run: | ${{ github.workspace }}/scripts/build_images.sh \ --tag=${{ matrix.job }} + - name: Scan ${{ matrix.job }} images + run: | + ${{ github.workspace }}/scripts/scan_images.sh \ + --tag=${{ matrix.job }} \ + --output-file=trivy-results-${{ matrix.job }}.sarif + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: trivy-results-${{ matrix.job }}.sarif + category: trivy-${{ matrix.job }} + + - name: Upload Trivy scan results as an artifact + uses: actions/upload-artifact@v2 + with: + name: trivy-${{ matrix.job }} + path: trivy-results-${{ matrix.job }}.sarif + retention-days: 7 + - name: Authenticate to Docker Hub if: github.event_name != 'pull_request' uses: docker/login-action@v2 @@ -91,5 +114,5 @@ jobs: - name: Push images to Docker Hub if: github.event_name != 'pull_request' run: | - ${{ github.workspace}}/scripts/push_images.sh \ - --tag=${{ matrix.job}} + ${{ github.workspace }}/scripts/push_images.sh \ + --tag=${{ matrix.job }} diff --git a/.gitignore b/.gitignore index b512c09..5344df7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -node_modules \ No newline at end of file +node_modules +*.sarif +tmp.* diff --git a/images/base/Dockerfile.centos b/images/base/Dockerfile.centos index 3c12014..6477aec 100644 --- a/images/base/Dockerfile.centos +++ b/images/base/Dockerfile.centos @@ -26,11 +26,9 @@ RUN dnf install --assumeyes epel-release && \ rsync && \ dnf clean all -# We use an old containerd.io because it contains a version of runc that works -# with sysbox correctly. RUN dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && \ dnf install --assumeyes \ - containerd.io-1.5.11-3.1.el8 \ + containerd.io \ docker-ce && \ systemctl enable docker diff --git a/images/base/Dockerfile.ubuntu b/images/base/Dockerfile.ubuntu index 1571ee2..1072a67 100644 --- a/images/base/Dockerfile.ubuntu +++ b/images/base/Dockerfile.ubuntu @@ -2,8 +2,6 @@ FROM ubuntu:20.04 SHELL ["/bin/bash", "-c"] -COPY containerd-pin /etc/apt/preferences.d/ - # Install the Docker apt repository RUN apt-get update && \ DEBIAN_FRONTEND="noninteractive" apt-get install --yes ca-certificates @@ -11,14 +9,12 @@ COPY docker-archive-keyring.gpg /usr/share/keyrings/docker-archive-keyring.gpg COPY docker.list /etc/apt/sources.list.d/docker.list # Install baseline packages -# We use an old containerd.io because it contains a version of runc that works -# with sysbox correctly. RUN apt-get update && \ DEBIAN_FRONTEND="noninteractive" apt-get install --yes \ bash \ build-essential \ ca-certificates \ - containerd.io=1.5.11-1 \ + containerd.io \ curl \ docker-ce \ docker-ce-cli \ diff --git a/images/base/containerd-pin b/images/base/containerd-pin deleted file mode 100644 index ed2ef50..0000000 --- a/images/base/containerd-pin +++ /dev/null @@ -1,3 +0,0 @@ -Package: containerd.io -Pin: version 1.5.11-1 -Pin-Priority: 999 \ No newline at end of file diff --git a/scripts/scan_images.sh b/scripts/scan_images.sh new file mode 100755 index 0000000..4cd4fdc --- /dev/null +++ b/scripts/scan_images.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# Avoid using cd because we accept paths as arguments to this script. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +check_dependencies \ + docker \ + trivy \ + jq + +source "$(dirname "${BASH_SOURCE[0]}")/images.sh" + +PROJECT_ROOT="$(git rev-parse --show-toplevel)" +TAG="ubuntu" +OUTPUT_FILE="" +DRY_RUN=false + +function usage() { + echo "Usage: $(basename "$0") [options]" + echo + echo "This script scans Coder's container images." + echo + echo "Options:" + echo " -h, --help Show this help text and exit" + echo " --dry-run Show commands that would run, but" + echo " do not run them" + echo " --tag= Select an image tag group to build," + echo " one of: centos, ubuntu)" + echo " --output-file= File path to write merged SARIF file to" + exit 1 +} + +# Allow a failing exit status, as user input can cause this +set +o errexit +options=$(getopt \ + --name="$(basename "$0")" \ + --longoptions=" \ + help, \ + dry-run, \ + tag:, \ + output-file:, \ + upload" \ + --options="h" \ + -- "$@") +# allow checking the exit code separately here, because we need both +# the response data and the exit code +# shellcheck disable=SC2181 +if [ $? -ne 0 ]; then + usage +fi +set -o errexit + +eval set -- "$options" +while true; do + case "${1:-}" in + --dry-run) + DRY_RUN=true + ;; + --tag) + shift + TAG="$1" + ;; + --output-file) + shift + OUTPUT_FILE="$1" + ;; + -h|--help) + usage + ;; + --) + shift + break + ;; + *) + # Default case, print an error and quit. This code shouldn't be + # reachable, because getopt should return an error exit code. + echo "Unknown option: $1" + usage + ;; + esac + shift +done + +if [ -z "${OUTPUT_FILE:-}" ]; then + echo "Output file must be specified" >&2 + usage +fi +OUTPUT_FILE="$(realpath "$OUTPUT_FILE")" +mkdir -p "$(dirname "$OUTPUT_FILE")" +if [ -e "$OUTPUT_FILE" ]; then + echo "Output file '$OUTPUT_FILE' already exists" >&2 + exit 1 +fi + +tmp_dir="$(mktemp -d)" + +# Trivy copies images to /tmp, so we need to set TMPDIR to a dir in the +# workspace dir to avoid running out of tmpfs space (which happens in CI). +trivy_tmp_dir="$(mktemp -d -p "$PROJECT_ROOT")" + +trap 'rm -rf "$tmp_dir" "$trivy_tmp_dir"' EXIT + +for image in "${IMAGES[@]}"; do + image_ref="codercom/enterprise-${image}:${TAG}" + image_name="${image}-${TAG}" + output="${tmp_dir}/${image}-${TAG}.sarif" + + if ! docker image inspect "$image_ref" >/dev/null 2>&1; then + echo "Image '$image_ref' does not exist locally; skipping" >&2 + continue + fi + + old_tmpdir="${TMPDIR:-}" + export TMPDIR="$trivy_tmp_dir" + + # The timeout is set to 15 minutes because in Java images it can take a while + # to scan JAR files for vulnerabilities. + run_trace $DRY_RUN trivy image \ + --severity CRITICAL,HIGH \ + --format sarif \ + --output "$output" \ + --timeout 15m0s \ + "$image_ref" 2>&1 | indent + + if [ "$old_tmpdir" = "" ]; then + unset TMPDIR + else + export TMPDIR="$old_tmpdir" + fi + + if [ $DRY_RUN = true ]; then + continue + fi + + if [ ! -f "$output" ]; then + echo "No SARIF output found for image '$image_ref' at '$output'" >&2 + exit 1 + fi + + # Do substitutions to add extra details to every message. Without these + # substitutions, most messages won't have any information about which image + # the vulnerability was found in. + jq \ + ".runs[].tool.driver.name |= \"Trivy ${image_name}\"" \ + "$output" >"$output.tmp" + mv "$output.tmp" "$output" + jq \ + ".runs[].results[].locations[].physicalLocation.artifactLocation.uri |= \"${image_name}/\" + ." \ + "$output" >"$output.tmp" + mv "$output.tmp" "$output" + jq \ + ".runs[].results[].locations[].message.text |= \"${image_name}: \" + ." \ + "$output" >"$output.tmp" + mv "$output.tmp" "$output" +done + +# Merge all SARIF files into one. +jq -s \ + 'reduce .[] as $item ([]; . + $item.runs) | { "version": "2.1.0", "$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json", "runs": . }' \ + "$tmp_dir"/*.sarif >"$OUTPUT_FILE"