Skip to content

Commit 0497f30

Browse files
committed
[SYNPY-1764] Add Trivy container vulnerability scanning to Docker build
Add Trivy scanning to gate Docker image publication on GHCR. Both release and develop Docker jobs now follow a build→scan→push pattern where images are only pushed if no Critical/High unfixed vulnerabilities are found. New workflows: - trivy.yml: reusable Trivy scanning workflow with SARIF upload to GitHub Security tab - docker_build.yml: reusable build/scan/push workflow for image rebuilds - trivy_periodic_scan.yml: daily rescan of latest published image with auto-remediation
1 parent f54c099 commit 0497f30

4 files changed

Lines changed: 404 additions & 26 deletions

File tree

.github/workflows/build.yml

Lines changed: 121 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -552,19 +552,81 @@ jobs:
552552
exit 1
553553
554554
# containerize the package and upload to the GHCR upon new release (whether pre-release or not)
555-
ghcr-build-and-push-on-release:
555+
# Step 1: Build the Docker image and save as tar for scanning
556+
ghcr-build-on-release:
556557
needs: deploy
557558
runs-on: ubuntu-latest
558559
permissions:
559560
contents: read
560561
packages: write
562+
outputs:
563+
image-tags: ${{ steps.set-tags.outputs.tags }}
564+
image-name: synapsepythonclient-release
565+
env:
566+
TARFILE_NAME: synapsepythonclient-release.tar
561567

562568
steps:
563569
- name: Check out the repo
564570
uses: actions/checkout@v4
565571
- name: Extract Release Version
566572
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
567573
shell: bash
574+
- name: Set image tags
575+
id: set-tags
576+
shell: bash
577+
run: |
578+
if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then
579+
echo "tags=ghcr.io/sage-bionetworks/synapsepythonclient:${{ env.RELEASE_VERSION }}-prerelease" >> $GITHUB_OUTPUT
580+
else
581+
echo "tags=ghcr.io/sage-bionetworks/synapsepythonclient:latest,ghcr.io/sage-bionetworks/synapsepythonclient:${{ env.RELEASE_VERSION }}" >> $GITHUB_OUTPUT
582+
fi
583+
- name: Set up Docker Buildx
584+
uses: docker/setup-buildx-action@v2
585+
- name: Build Docker image
586+
uses: docker/build-push-action@v5
587+
with:
588+
context: .
589+
push: false
590+
load: true
591+
provenance: false
592+
tags: synapsepythonclient-release:local
593+
file: ./Dockerfile
594+
platforms: linux/amd64
595+
cache-from: type=registry,ref=ghcr.io/sage-bionetworks/synapsepythonclient:build-cache
596+
- name: Save Docker image to tar
597+
run: docker save synapsepythonclient-release:local -o ${{ env.TARFILE_NAME }}
598+
- name: Upload tar artifact
599+
uses: actions/upload-artifact@v4
600+
with:
601+
name: ${{ env.TARFILE_NAME }}
602+
path: ${{ env.TARFILE_NAME }}
603+
retention-days: 1
604+
605+
# Step 2: Scan the built image with Trivy before pushing
606+
trivy-scan-release:
607+
needs: [ghcr-build-on-release]
608+
uses: ./.github/workflows/trivy.yml
609+
with:
610+
SOURCE_TYPE: tar
611+
TARFILE_NAME: synapsepythonclient-release.tar
612+
IMAGE_NAME: synapsepythonclient-release:local
613+
EXIT_CODE: 1
614+
permissions:
615+
contents: read
616+
security-events: write
617+
actions: read
618+
619+
# Step 3: Push the image to GHCR only if Trivy scan passes
620+
ghcr-push-on-release:
621+
needs: [ghcr-build-on-release, trivy-scan-release]
622+
runs-on: ubuntu-latest
623+
permissions:
624+
contents: read
625+
packages: write
626+
627+
steps:
628+
- name: Check out the repo
629+
uses: actions/checkout@v4
568630
- name: Set up Docker Buildx
569631
uses: docker/setup-buildx-action@v2
570632
- name: Log in to GitHub Container Registry
@@ -573,39 +635,74 @@ jobs:
573635
registry: ghcr.io
574636
username: ${{ github.actor }}
575637
password: ${{ secrets.GITHUB_TOKEN }}
576-
- name: Build and push Docker image (official release)
577-
id: docker_build
578-
if: '!github.event.release.prerelease'
579-
uses: docker/build-push-action@v3
638+
- name: Build and push Docker image
639+
uses: docker/build-push-action@v5
580640
with:
641+
context: .
581642
push: true
582643
provenance: false
583-
tags: ghcr.io/sage-bionetworks/synapsepythonclient:latest,ghcr.io/sage-bionetworks/synapsepythonclient:${{ env.RELEASE_VERSION }}
644+
tags: ${{ needs.ghcr-build-on-release.outputs.image-tags }}
584645
file: ./Dockerfile
585646
platforms: linux/amd64
586647
cache-from: type=registry,ref=ghcr.io/sage-bionetworks/synapsepythonclient:build-cache
587648
cache-to: type=registry,mode=max,ref=ghcr.io/sage-bionetworks/synapsepythonclient:build-cache
588-
- name: Build and push Docker image (pre-release)
589-
id: docker_build_prerelease
590-
if: 'github.event.release.prerelease'
591-
uses: docker/build-push-action@v3
649+
650+
# containerize the package and upload to the GHCR upon commit in develop
651+
# Step 1: Build the Docker image and save as tar for scanning
652+
ghcr-build-on-develop:
653+
runs-on: ubuntu-latest
654+
if: github.ref == 'refs/heads/develop'
655+
permissions:
656+
contents: read
657+
packages: write
658+
outputs:
659+
image-tag: ghcr.io/sage-bionetworks/synapsepythonclient:develop-${{ github.sha }}
660+
image-name: synapsepythonclient-develop
661+
env:
662+
TARFILE_NAME: synapsepythonclient-develop.tar
663+
664+
steps:
665+
- name: Check out the repo
666+
uses: actions/checkout@v4
667+
- name: Set up Docker Buildx
668+
uses: docker/setup-buildx-action@v2
669+
- name: Build Docker image
670+
uses: docker/build-push-action@v5
592671
with:
593-
push: true
672+
context: .
673+
push: false
674+
load: true
594675
provenance: false
595-
tags: ghcr.io/sage-bionetworks/synapsepythonclient:${{ env.RELEASE_VERSION }}-prerelease
676+
tags: synapsepythonclient-develop:local
596677
file: ./Dockerfile
597678
platforms: linux/amd64
598-
cache-from: type=registry,ref=ghcr.io/sage-bionetworks/synapsepythonclient:build-cache-prerelease
599-
cache-to: type=registry,mode=max,ref=ghcr.io/sage-bionetworks/synapsepythonclient:build-cache-prerelease
600-
- name: Output image digest (official release)
601-
if: '!github.event.release.prerelease'
602-
run: echo "The image digest for official release is ${{ steps.docker_build.outputs.digest }}"
603-
- name: Output image digest (pre-release)
604-
if: 'github.event.release.prerelease'
605-
run: echo "The image digest for pre-release is ${{ steps.docker_build_prerelease.outputs.digest }}"
679+
cache-from: type=registry,ref=ghcr.io/sage-bionetworks/synapsepythonclient:build-cache
680+
- name: Save Docker image to tar
681+
run: docker save synapsepythonclient-develop:local -o ${{ env.TARFILE_NAME }}
682+
- name: Upload tar artifact
683+
uses: actions/upload-artifact@v4
684+
with:
685+
name: ${{ env.TARFILE_NAME }}
686+
path: ${{ env.TARFILE_NAME }}
687+
retention-days: 1
688+
689+
# Step 2: Scan the built image with Trivy before pushing
690+
trivy-scan-develop:
691+
needs: [ghcr-build-on-develop]
692+
uses: ./.github/workflows/trivy.yml
693+
with:
694+
SOURCE_TYPE: tar
695+
TARFILE_NAME: synapsepythonclient-develop.tar
696+
IMAGE_NAME: synapsepythonclient-develop:local
697+
EXIT_CODE: 1
698+
permissions:
699+
contents: read
700+
security-events: write
701+
actions: read
606702

607-
# containerize the package and upload to the GHCR upon commit in develop
608-
ghcr-build-and-push-on-develop:
703+
# Step 3: Push the image to GHCR only if Trivy scan passes
704+
ghcr-push-on-develop:
705+
needs: [ghcr-build-on-develop, trivy-scan-develop]
609706
runs-on: ubuntu-latest
610707
if: github.ref == 'refs/heads/develop'
611708
permissions:
@@ -623,16 +720,14 @@ jobs:
623720
registry: ghcr.io
624721
username: ${{ github.actor }}
625722
password: ${{ secrets.GITHUB_TOKEN }}
626-
- name: Build and push Docker image for develop
627-
id: docker_build
723+
- name: Build and push Docker image
628724
uses: docker/build-push-action@v5
629725
with:
726+
context: .
630727
push: true
631728
provenance: false
632729
tags: ghcr.io/sage-bionetworks/synapsepythonclient:develop-${{ github.sha }}
633730
file: ./Dockerfile
634731
platforms: linux/amd64
635732
cache-from: type=registry,ref=ghcr.io/sage-bionetworks/synapsepythonclient:build-cache
636733
cache-to: type=inline
637-
- name: Output image digest
638-
run: echo "The image digest is ${{ steps.docker_build.outputs.digest }}"

.github/workflows/docker_build.yml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
#
3+
# Reusable workflow to build, scan, and push a Docker image.
4+
# Called by the periodic scan workflow to rebuild images
5+
# when new vulnerabilities are found.
6+
#
7+
name: Build and publish a Docker image
8+
9+
on:
10+
workflow_call:
11+
inputs:
12+
REF_TO_CHECKOUT:
13+
required: false
14+
type: string
15+
description: "Reference to checkout, e.g. a tag like v1.0.1. Defaults to the branch/tag of the current event."
16+
IMAGE_REFERENCES:
17+
required: true
18+
type: string
19+
description: "Comma-separated image references, e.g., ghcr.io/sage-bionetworks/synapsepythonclient:1.0.1"
20+
21+
env:
22+
TARFILE_NAME: image.tar
23+
LOCAL_IMAGE_TAG: rebuild-image:local
24+
25+
jobs:
26+
build:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v4
31+
with:
32+
ref: ${{ inputs.REF_TO_CHECKOUT }}
33+
34+
- name: Set up Docker Buildx
35+
uses: docker/setup-buildx-action@v2
36+
37+
- name: Build Docker image
38+
uses: docker/build-push-action@v5
39+
with:
40+
context: .
41+
push: false
42+
load: true
43+
tags: rebuild-image:local
44+
file: ./Dockerfile
45+
platforms: linux/amd64
46+
47+
- name: Save Docker image to tar
48+
run: docker save rebuild-image:local -o ${{ env.TARFILE_NAME }}
49+
50+
- name: Upload tarball for use by Trivy job
51+
uses: actions/upload-artifact@v4
52+
with:
53+
name: ${{ env.TARFILE_NAME }}
54+
path: ${{ env.TARFILE_NAME }}
55+
retention-days: 1
56+
57+
outputs:
58+
tarfile_artifact: ${{ env.TARFILE_NAME }}
59+
60+
trivy-scan:
61+
needs: build
62+
uses: "./.github/workflows/trivy.yml"
63+
with:
64+
SOURCE_TYPE: tar
65+
IMAGE_NAME: rebuild-image:local
66+
TARFILE_NAME: ${{ needs.build.outputs.tarfile_artifact }}
67+
EXIT_CODE: 1
68+
permissions:
69+
contents: read
70+
security-events: write
71+
actions: read
72+
73+
push-image:
74+
needs: [build, trivy-scan]
75+
runs-on: ubuntu-latest
76+
permissions:
77+
contents: read
78+
packages: write
79+
steps:
80+
- name: Download tar artifact
81+
uses: actions/download-artifact@v4
82+
with:
83+
name: ${{ needs.build.outputs.tarfile_artifact }}
84+
path: /tmp
85+
86+
- name: Load Docker image from tar
87+
run: docker load -i /tmp/${{ needs.build.outputs.tarfile_artifact }}
88+
89+
- name: Login to GitHub Container Registry
90+
uses: docker/login-action@v2
91+
with:
92+
registry: ghcr.io
93+
username: ${{ github.actor }}
94+
password: ${{ secrets.GITHUB_TOKEN }}
95+
96+
- name: Tag and push Docker image
97+
shell: bash
98+
run: |
99+
IFS=',' read -ra TAGS <<< "${{ inputs.IMAGE_REFERENCES }}"
100+
for TAG in "${TAGS[@]}"; do
101+
docker tag rebuild-image:local "$TAG"
102+
docker push "$TAG"
103+
done

.github/workflows/trivy.yml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
#
3+
# This workflow runs Trivy on a Docker image
4+
# It can pull the image from a container registry
5+
# or download a tar file. The latter is used
6+
# to check a container image prior to publishing
7+
# to the registry.
8+
9+
name: Run Trivy on a Docker image and push results to GitHub
10+
11+
on:
12+
workflow_call:
13+
inputs:
14+
SOURCE_TYPE: # 'tar' or 'image'
15+
required: true
16+
type: string
17+
TARFILE_NAME: # only used if SOURCE_TYPE=='tar'
18+
required: false
19+
type: string
20+
IMAGE_NAME:
21+
required: true
22+
type: string
23+
EXIT_CODE: # return code for failed scan. 0 means OK. Non-zero will fail the build when there are findings.
24+
required: false
25+
type: number
26+
default: 0
27+
outputs:
28+
trivy_conclusion:
29+
description: "The pass/fail status from Trivy"
30+
value: ${{ jobs.trivy.outputs.trivy_conclusion }}
31+
32+
env:
33+
sarif_file_name: trivy-results.sarif
34+
# downloading the trivy-db from its default GitHub location fails because
35+
# the site experiences too many downloads. The fix is to pull from this
36+
# alternate location.
37+
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2
38+
TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db:1
39+
40+
jobs:
41+
trivy:
42+
name: Trivy
43+
runs-on: ubuntu-latest
44+
permissions:
45+
contents: read
46+
security-events: write
47+
48+
steps:
49+
- name: Checkout repository
50+
uses: actions/checkout@v4
51+
52+
- name: Download tar file
53+
id: tar-download
54+
uses: actions/download-artifact@v4
55+
if: ${{ inputs.SOURCE_TYPE == 'tar' }}
56+
with:
57+
name: ${{ inputs.TARFILE_NAME }}
58+
path: /tmp
59+
60+
- name: Load docker image from tar file
61+
if: ${{ inputs.SOURCE_TYPE == 'tar' }}
62+
run: docker load -i ${{ steps.tar-download.outputs.download-path }}/${{ inputs.TARFILE_NAME }}
63+
64+
- name: Run Trivy vulnerability scanner for any major issues
65+
uses: aquasecurity/trivy-action@0.32.0
66+
id: trivy
67+
with:
68+
image-ref: ${{ inputs.IMAGE_NAME }}
69+
ignore-unfixed: true # skip vulnerabilities for which there is no fix
70+
severity: 'CRITICAL,HIGH'
71+
format: 'sarif'
72+
limit-severities-for-sarif: true
73+
output: ${{ env.sarif_file_name }}
74+
exit-code: ${{ inputs.EXIT_CODE }}
75+
76+
- name: Upload Trivy scan results to GitHub Security tab
77+
uses: github/codeql-action/upload-sarif@v3.25.12
78+
if: ${{ success() || steps.trivy.conclusion == 'failure' }}
79+
with:
80+
sarif_file: ${{ env.sarif_file_name }}
81+
wait-for-processing: true
82+
83+
- name: Upload Trivy output
84+
uses: actions/upload-artifact@v4
85+
if: ${{ success() || steps.trivy.conclusion == 'failure' }}
86+
with:
87+
name: ${{ env.sarif_file_name }}
88+
path: ${{ env.sarif_file_name }}
89+
90+
outputs:
91+
trivy_conclusion: ${{ steps.trivy.conclusion }}

0 commit comments

Comments
 (0)