Vuln. scan #798
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is a GitHub workflow defining a set of jobs with a set of steps. | |
# ref: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions | |
# | |
# This workflow use aquasecurity/trivy to scan the images we have published for | |
# known vulnerabilities. If there are such that can be patched, we let this | |
# workflow fail to signal that unless we make an exception, which we do for the | |
# singleuser-sample image only. | |
# | |
# | |
# About environment: watch-dependencies | |
# | |
# To reduce the exposure of the secrets.jupyterhub_bot_pat token that was setup | |
# for the environment watch-dependencies, we have setup a dedicated environment | |
# according to steps in | |
# https://github.com/jupyterhub/team-compass/issues/516#issuecomment-1129961954. | |
# | |
name: Vuln. scan | |
on: | |
pull_request: | |
paths: | |
- ".github/workflows/vuln-scan.yaml" | |
push: | |
paths: | |
- ".github/workflows/vuln-scan.yaml" | |
branches: ["main"] | |
schedule: | |
# At 05:00 on Monday - https://crontab.guru | |
- cron: "0 5 * * 1" | |
workflow_dispatch: | |
jobs: | |
trivy_image_scan: | |
if: github.repository == 'jupyterhub/zero-to-jupyterhub-k8s' | |
runs-on: ubuntu-22.04 | |
environment: watch-dependencies | |
strategy: | |
fail-fast: false | |
matrix: | |
include: | |
- image_ref: hub | |
accept_failure: false | |
- image_ref: secret-sync | |
accept_failure: false | |
- image_ref: network-tools | |
accept_failure: false | |
- image_ref: image-awaiter | |
accept_failure: false | |
- image_ref: singleuser-sample | |
accept_failure: false | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
# chartpress requires git history to set chart version and image tags | |
# correctly | |
fetch-depth: 0 | |
- uses: actions/setup-python@v5 | |
with: | |
python-version: "3.11" | |
- name: Install chartpress | |
run: | | |
pip install chartpress | |
# charpress --list-images output lines of name:tag format. We use it with | |
# a search string in matrix.image_ref to find the specific image to scan | |
# in this job. | |
- name: Identify image name:tag | |
id: image | |
run: | | |
IMAGE_SPEC=$( | |
chartpress --list-images \ | |
| grep ${{ matrix.image_ref }}: | |
) | |
echo "Identified image: $IMAGE_SPEC" | |
echo "spec=$IMAGE_SPEC" >> $GITHUB_OUTPUT | |
echo "name=$(echo $IMAGE_SPEC | sed 's/\(.*\):.*/\1/')" >> $GITHUB_OUTPUT | |
echo "tag=$(echo $IMAGE_SPEC | sed 's/.*:\(.*\)/\1/')" >> $GITHUB_OUTPUT | |
- name: Create ./tmp dir | |
run: mkdir ./tmp | |
# Action reference: https://github.com/aquasecurity/trivy-action | |
- name: Scan latest published image | |
id: scan_1 | |
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 | |
with: | |
image-ref: ${{ steps.image.outputs.spec }} | |
format: json # ref: https://github.com/aquasecurity/trivy#save-the-results-as-json | |
output: tmp/scan_1.json | |
ignore-unfixed: true | |
exit-code: "1" | |
# Keep running the subsequent steps of the job, they are made to | |
# explicitly adjust based on this step's outcome. | |
continue-on-error: true | |
# Steps below is only executing if vulnerabilities have been detected. | |
# ----------------------------------------------------------------------- | |
- name: Rebuild image | |
id: rebuild | |
if: steps.scan_1.outcome == 'failure' | |
env: | |
DOCKER_BUILDKIT: "1" | |
run: | | |
docker build -t rebuilt-image images/${{ matrix.image_ref }} | |
- name: Scan rebuilt image | |
id: scan_2 | |
if: steps.rebuild.outcome == 'success' | |
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 | |
with: | |
image-ref: rebuilt-image | |
format: json # ref: https://github.com/aquasecurity/trivy#save-the-results-as-json | |
output: tmp/scan_2.json | |
ignore-unfixed: true | |
# Analyze the scan reports. If they differ, we want to proceed and create | |
# or update a PR. We use a hash from the final scan report as an | |
# indication to rebuild or not. | |
- name: Analyze scan reports | |
id: analyze | |
if: steps.rebuild.outcome == 'success' | |
run: | | |
echo "utc_time=$(date --utc +'%F_%T')" >> $GITHUB_OUTPUT | |
json_to_misc() { | |
# Count vulnerabilities | |
VULNERABILITY_COUNT="$(cat tmp/scan_$1.json | jq -r '[.Results[].Vulnerabilities | select(type != null)] | add | select(. != null) | length')" | |
echo "VULNERABILITY_COUNT_$1=$VULNERABILITY_COUNT" >> $GITHUB_ENV | |
# Construct a markdown summary | |
if [[ "$VULNERABILITY_COUNT" == "0" ]]; then | |
echo "No vulnerabilities! :tada:" >> tmp/md_summary_$1.md | |
else | |
echo "Target | Vuln. ID | Package Name | Installed v. | Fixed v." >> tmp/md_summary_$1.md | |
echo "-|-|-|-|-" >> tmp/md_summary_$1.md | |
cat tmp/scan_$1.json | jq -r '.Results[] | select(.Vulnerabilities != null) | .Type + " | " + (.Vulnerabilities[] | .VulnerabilityID + " | " + .PkgName + " | " + .InstalledVersion + " | " + .FixedVersion)' | sort >> tmp/md_summary_$1.md | |
fi | |
# Set a multiline string output with the following technique: | |
# ref: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings | |
# | |
eof_marker=EOF_$RANDOM | |
echo "md_summary_$1<<$eof_marker" >> $GITHUB_OUTPUT | |
cat tmp/md_summary_$1.md >> $GITHUB_OUTPUT | |
echo "$eof_marker" >> $GITHUB_OUTPUT | |
# Calculate a hash of the markdown summary | |
HASH=$(cat tmp/md_summary_$1.md | sha1sum) | |
HASH=${HASH:0:10} | |
export HASH_$1=$HASH | |
echo "hash_$1=$HASH" >> $GITHUB_OUTPUT | |
} | |
json_to_misc 1 | |
json_to_misc 2 | |
# Did rebuilding the image change anything? | |
if [ "$HASH_1" == "$HASH_2" ]; then | |
echo "proceed=no" >> $GITHUB_OUTPUT | |
echo "No vulnerabilities were patched by rebuilding the image - won't proceed!" | |
else | |
echo "proceed=yes" >> $GITHUB_OUTPUT | |
echo "Vulnerabilities were patched by rebuilding the image - will proceed!" | |
fi | |
- name: Describe vulnerabilities | |
if: steps.rebuild.outcome == 'success' | |
uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 | |
with: | |
image-ref: rebuilt-image | |
format: table | |
ignore-unfixed: true | |
- name: Decision to not proceed | |
if: steps.analyze.outputs.proceed == 'no' | |
run: | | |
echo "::warning::None of the $VULNERABILITY_COUNT_1 vulnerabilities got patched by rebuilding the image :(" | |
continue-on-error: ${{ matrix.accept_failure == true }} | |
# Steps below are executed if the analyze step decided to proceed. | |
# ----------------------------------------------------------------------- | |
# Searches for the "# VULN_SCAN_TIME=..." in the Dockerfile and sets a new | |
# value, which can be used to submit a PR that when merged will trigger a | |
# rebuild of the image. | |
- name: Update VULN_SCAN_TIME in Dockerfile | |
if: steps.analyze.outputs.proceed == 'yes' | |
run: | | |
value_to_set="${{ steps.analyze.outputs.utc_time }}" | |
file_to_update="images/${{ matrix.image_ref }}/Dockerfile" | |
sed --in-place "s/\(#.*VULN_SCAN_TIME=\)\(.*\)/\1${value_to_set}/" "$file_to_update" | |
git --no-pager diff --color=always | |
# The create-pull-request action is smart enough to only create/update a | |
# PR if there is a change to anything not .gitignored. A change will be | |
# made only if the analyze steps outputted hash is changed. | |
# | |
# ref: https://github.com/peter-evans/create-pull-request | |
- name: Create or update a PR | |
if: steps.analyze.outputs.proceed == 'yes' && github.event_name != 'pull_request' | |
uses: peter-evans/create-pull-request@v7 | |
with: | |
token: "${{ secrets.jupyterhub_bot_pat }}" | |
author: JupterHub Bot Account <105740858+jupyterhub-bot@users.noreply.github.com> | |
committer: JupterHub Bot Account <105740858+jupyterhub-bot@users.noreply.github.com> | |
reviewers: consideratio | |
branch: vuln-scan-${{ matrix.image_ref }} | |
title: Vulnerability patch in ${{ matrix.image_ref }} | |
labels: image:rebuild-to-patch-vuln | |
body: | | |
A rebuild of `${{ steps.image.outputs.name }}` has been found to influence the detected vulnerabilities! This PR will trigger a rebuild because it has updated a comment in the Dockerfile. | |
## About | |
This scan for known vulnerabilities has been made by [aquasecurity/trivy](https://github.com/aquasecurity/trivy). Trivy was configured to filter the vulnerabilities with the following settings: | |
- ignore-unfixed: `true` | |
## Before | |
Before trying to rebuild the image, the following vulnerabilities was detected in `${{ steps.image.outputs.spec }}`. | |
${{ steps.analyze.outputs.md_summary_1 }} | |
## After | |
${{ steps.analyze.outputs.md_summary_2 }} | |
commit-message: | | |
Patch known vulnerability in ${{ matrix.image_ref }} |