Security #924
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
name: Security | |
on: | |
schedule: | |
- cron: "30 5 * * MON-FRI" # Every weekday at 05:30 UTC | |
workflow_dispatch: | |
push: | |
branches: | |
- main | |
paths: | |
- '**/.trivyignore' | |
jobs: | |
get-projects: | |
runs-on: ubuntu-latest | |
outputs: | |
projects: ${{ steps.get-projects.outputs.projects }} | |
steps: | |
- uses: actions/checkout@v4 | |
- id: get-projects | |
run: echo "projects=$(find projects -mindepth 1 -maxdepth 1 -printf "%f\n" | jq --raw-input . | jq --slurp --compact-output .)" | tee -a "$GITHUB_OUTPUT" | |
trivy-scan: | |
runs-on: ubuntu-latest | |
needs: | |
- get-projects | |
strategy: | |
fail-fast: false | |
matrix: | |
project: ${{ fromJson(needs.get-projects.outputs.projects) }} | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Add new line to .trivyignore files | |
# to ensure files exist, and they can be merged correctly by Trivy | |
run: | | |
echo >> .trivyignore | |
echo >> projects/${{ matrix.project }}/.trivyignore | |
- name: Scan image | |
uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0 | |
with: | |
image-ref: 'ghcr.io/ministryofjustice/hmpps-probation-integration-services/${{ matrix.project }}:latest' | |
ignore-unfixed: true | |
severity: 'CRITICAL,HIGH' | |
exit-code: '0' | |
format: 'sarif' | |
output: 'trivy-results.sarif' | |
trivyignores: '.trivyignore,projects/${{ matrix.project }}/.trivyignore' | |
limit-severities-for-sarif: true | |
env: | |
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 | |
TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1 | |
- name: Upload Trivy scan results to GitHub Security tab | |
uses: github/codeql-action/upload-sarif@v3 | |
if: always() | |
with: | |
sarif_file: 'trivy-results.sarif' | |
- name: Get Trivy results | |
uses: aquasecurity/trivy-action@18f2510ee396bbf400402947b394f2dd8c87dbb0 # v0.29.0 | |
with: | |
image-ref: 'ghcr.io/ministryofjustice/hmpps-probation-integration-services/${{ matrix.project }}:latest' | |
ignore-unfixed: true | |
severity: 'CRITICAL,HIGH' | |
format: 'json' | |
output: 'trivy.json' | |
trivyignores: '.trivyignore,projects/${{ matrix.project }}/.trivyignore' | |
env: | |
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2 | |
TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1 | |
- name: Output results | |
run: | | |
jq -c '{"${{ matrix.project }}": .Results[].Vulnerabilities | select(. != null) | flatten}' trivy.json | tee results.json | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
- uses: actions/upload-artifact@v4 | |
with: | |
name: trivy-results-${{ matrix.project }} | |
path: results.json | |
trivy-merge: | |
runs-on: ubuntu-latest | |
needs: | |
- trivy-scan | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/download-artifact@v4 | |
with: | |
pattern: trivy-results-* | |
path: results | |
- name: Merge results | |
run: | | |
find results -maxdepth 2 -name results.json -exec cat {} \; | jq -c --slurp 'map(to_entries | map(.key as $matrix_key | .value | map_values({($matrix_key): .}))) | flatten | reduce .[] as $item ({}; . * $item)' | tee results.json | |
- name: Create GitHub issues | |
run: | | |
jq -c 'to_entries | map((.value // empty) + {Projects: [.key]}) | |
| flatten | |
| group_by(.VulnerabilityID) | |
| map(reduce .[] as $vuln (.[0] + {Locations:[]}; .Projects += $vuln.Projects | .Locations += [$vuln.PkgName + ":" + $vuln.InstalledVersion + " (" + $vuln.PkgPath + ")"])) | |
| map_values({Title: .VulnerabilityID, Body: ("### " + .Title + "\n" + .PrimaryURL + "\n>" + .Description + "\n#### Projects:\n* " + (.Projects | sort | unique | join("\n* ")) + "\n#### Locations:\n* `" + (.Locations | sort | unique | join("`\n* `")) + "`\n#### References:\n* " + (.References | sort | unique | join("\n* ")))}) | |
| .[]' < results.json \ | |
| while read -r vulnerability; do | |
title=$(jq -r '.Title' <<< "$vulnerability") | |
jq -r '.Body' <<< "$vulnerability" | tee "${title}-body.json" | |
existing=$(gh issue list --state open --label dependencies --label security --search "$title" --json 'number' --jq '.[].number' | head -n1) | |
if [ -n "$existing" ]; then | |
echo "Issue '$title' already exists, updating body..." | |
gh issue edit "$existing" --body-file "${title}-body.json" | |
else | |
gh issue create --title "$title" --body-file "${title}-body.json" --label 'dependencies,security' | |
fi | |
done | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
- name: Close GitHub issues | |
run: | | |
openissues="$(gh issue list --state open --label dependencies --label security | awk '{print $3}')" | |
scanresults="$(jq -r -c 'with_entries(select(.value != null)) | .[].VulnerabilityID' < results.json | sort -u)" | |
issuestoclose="$(comm -23 <(echo "$openissues" | sort -u) <(echo "$scanresults" | sort -u))" #print lines only present in first file | |
echo "openissues=$openissues" | |
echo "scanresults=$scanresults" | |
echo "issuestoclose=$issuestoclose" | |
for cve in $issuestoclose; do | |
echo "$cve is already resolved, removing matching issue..." | |
issuenumber=$(gh issue list --state open --label dependencies --label security --search "$cve" | awk '{print $1}') | |
echo "$issuenumber" | xargs -n1 gh issue close | |
done | |
env: | |
GITHUB_TOKEN: ${{ github.token }} | |
- name: Fail job if any vulnerabilities are found | |
run: if [ "$(jq '. | with_entries(select(.value != null)) | length' < results.json)" != 0 ]; then exit 1; fi | |
veracode-scan: | |
runs-on: ubuntu-latest | |
needs: | |
- get-projects | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: ./.github/actions/setup-gradle | |
with: | |
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} | |
cache-read-only: true | |
- name: Build jars | |
run: ./gradlew jar | |
- name: Package jars | |
run: find . -name '*.jar' | zip -r package.zip -@ | |
- name: Select an API key | |
run: | | |
echo "Randomly picking 1 of 5 api keys, to help avoid veracode API rate limits." | |
X=$(( RANDOM % 5 )) | |
VERACODE_API_ID_X="VERACODE_API_ID_${X}" | |
VERACODE_API_KEY_X="VERACODE_API_KEY_${X}" | |
echo VERACODE_API_ID="${!VERACODE_API_ID_X}" >> $GITHUB_ENV | |
echo VERACODE_API_KEY="${!VERACODE_API_KEY_X}" >> $GITHUB_ENV | |
echo "Using VERACODE_API_ID_${X} from pool (${VERACODE_API_ID:0:5}...)" | |
env: | |
VERACODE_API_ID_0: ${{ secrets.HMPPS_VERACODE_API_ID_0 }} | |
VERACODE_API_ID_1: ${{ secrets.HMPPS_VERACODE_API_ID_1 }} | |
VERACODE_API_ID_2: ${{ secrets.HMPPS_VERACODE_API_ID_2 }} | |
VERACODE_API_ID_3: ${{ secrets.HMPPS_VERACODE_API_ID_3 }} | |
VERACODE_API_ID_4: ${{ secrets.HMPPS_VERACODE_API_ID_4 }} | |
VERACODE_API_KEY_0: ${{ secrets.HMPPS_VERACODE_API_KEY_0 }} | |
VERACODE_API_KEY_1: ${{ secrets.HMPPS_VERACODE_API_KEY_1 }} | |
VERACODE_API_KEY_2: ${{ secrets.HMPPS_VERACODE_API_KEY_2 }} | |
VERACODE_API_KEY_3: ${{ secrets.HMPPS_VERACODE_API_KEY_3 }} | |
VERACODE_API_KEY_4: ${{ secrets.HMPPS_VERACODE_API_KEY_4 }} | |
- name: Upload to Veracode | |
uses: veracode/[email protected] | |
with: | |
appname: hmpps-probation-integration-services | |
createprofile: false | |
deleteincompletescan: 2 # force delete any incomplete scans | |
filepath: package.zip | |
vid: ${{ env.VERACODE_API_ID }} | |
vkey: ${{ env.VERACODE_API_KEY }} |