Skip to content

Commit 551a20b

Browse files
authored
Merge pull request #207 from NHSDigital/feature/CCM-1725_grype
CCM-17525: grype report
2 parents 7d07ec8 + 1ae83ff commit 551a20b

5 files changed

Lines changed: 216 additions & 27 deletions

File tree

.github/actions/scan-dependencies/action.yaml

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,52 +19,139 @@ inputs:
1919
idp_aws_report_upload_bucket_endpoint:
2020
description: "IDP AWS report upload endpoint to upload the report to"
2121
required: false
22+
skip_if_pr_has_label:
23+
description: "Skip dependency scanning when the triggering PR has this label"
24+
required: false
25+
default: "skip-dependencies-check"
2226
runs:
2327
using: "composite"
2428
steps:
29+
- name: "Check if dependency scan should be skipped"
30+
id: skip-check
31+
shell: bash
32+
env:
33+
GH_TOKEN: ${{ github.token }}
34+
run: |
35+
SHOULD_SKIP="false"
36+
37+
# Only check labels if a skip label is configured
38+
if [[ -n "${{ inputs.skip_if_pr_has_label }}" ]]; then
39+
PR_NUMBER=""
40+
41+
# Try to get PR number from event context first (for direct PR triggers)
42+
if [[ -n "${{ github.event.pull_request.number }}" ]]; then
43+
PR_NUMBER="${{ github.event.pull_request.number }}"
44+
fi
45+
46+
# If no PR number from event, try to find it from the branch (for workflow_call context)
47+
if [[ -z "${PR_NUMBER}" ]]; then
48+
REF="${{ github.ref }}"
49+
if [[ "${REF}" == refs/heads/* ]]; then
50+
BRANCH_NAME="${REF#refs/heads/}"
51+
# Find PR for this branch
52+
RESULT=$(gh pr list --head "${BRANCH_NAME}" --limit 1 --state open --json number --jq '.[0].number' 2>/dev/null || echo "")
53+
if [[ -n "${RESULT}" && "${RESULT}" != "null" ]]; then
54+
PR_NUMBER="${RESULT}"
55+
fi
56+
elif [[ "${REF}" == refs/pull/* ]]; then
57+
PR_NUMBER="${REF#refs/pull/}"
58+
PR_NUMBER="${PR_NUMBER%%/*}"
59+
fi
60+
fi
61+
62+
# If we have a PR number, query its labels
63+
if [[ -n "${PR_NUMBER}" ]]; then
64+
if gh pr view "${PR_NUMBER}" --json labels --jq '.labels[].name' | grep -Fxq "${{ inputs.skip_if_pr_has_label }}"; then
65+
SHOULD_SKIP="true"
66+
fi
67+
fi
68+
fi
69+
70+
echo "should_skip=${SHOULD_SKIP}" >> "$GITHUB_OUTPUT"
71+
- name: "Skip dependency scan"
72+
if: steps.skip-check.outputs.should_skip == 'true'
73+
shell: bash
74+
run: |
75+
echo "Dependency scan skipped because PR has label '${{ inputs.skip_if_pr_has_label }}'."
2576
- name: "Generate SBOM"
77+
if: steps.skip-check.outputs.should_skip != 'true'
2678
shell: bash
2779
run: |
80+
ACTION_ROOT="$(cd "${GITHUB_ACTION_PATH}/../../.." && pwd)"
81+
export TOOLING_ROOT="${ACTION_ROOT}"
2882
export BUILD_DATETIME=${{ inputs.build_datetime }}
29-
./scripts/reports/create-sbom-report.sh
83+
"${ACTION_ROOT}/scripts/reports/create-sbom-report.sh"
3084
- name: "Compress SBOM report"
85+
if: steps.skip-check.outputs.should_skip != 'true'
3186
shell: bash
3287
run: zip sbom-repository-report.json.zip sbom-repository-report.json
3388
- name: "Upload SBOM report as an artefact"
34-
if: ${{ !env.ACT }}
89+
if: ${{ !env.ACT && steps.skip-check.outputs.should_skip != 'true' }}
3590
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
3691
with:
3792
name: sbom-repository-report.json.zip
3893
path: ./sbom-repository-report.json.zip
3994
retention-days: 21
4095
- name: "Scan vulnerabilities"
96+
if: steps.skip-check.outputs.should_skip != 'true'
4197
shell: bash
4298
run: |
99+
ACTION_ROOT="$(cd "${GITHUB_ACTION_PATH}/../../.." && pwd)"
100+
export TOOLING_ROOT="${ACTION_ROOT}"
43101
export BUILD_DATETIME=${{ inputs.build_datetime }}
44-
./scripts/reports/scan-vulnerabilities.sh
102+
"${ACTION_ROOT}/scripts/reports/scan-vulnerabilities.sh"
103+
- name: "Generate vulnerabilities summary"
104+
if: steps.skip-check.outputs.should_skip != 'true'
105+
shell: bash
106+
run: |
107+
ACTION_ROOT="$(cd "${GITHUB_ACTION_PATH}/../../.." && pwd)"
108+
"${ACTION_ROOT}/scripts/reports/parse-vulnerabilities.sh" vulnerabilities-repository-report.json | tee vulnerabilities-summary.md
109+
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
110+
cat vulnerabilities-summary.md >> "$GITHUB_STEP_SUMMARY"
111+
fi
45112
- name: "Compress vulnerabilities report"
113+
if: steps.skip-check.outputs.should_skip != 'true'
46114
shell: bash
47115
run: zip vulnerabilities-repository-report.json.zip vulnerabilities-repository-report.json
48116
- name: "Upload vulnerabilities report as an artefact"
49-
if: ${{ !env.ACT }}
117+
if: ${{ !env.ACT && steps.skip-check.outputs.should_skip != 'true' }}
50118
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
51119
with:
52120
name: vulnerabilities-repository-report.json.zip
53121
path: ./vulnerabilities-repository-report.json.zip
54122
retention-days: 21
123+
- name: "Upload vulnerabilities summary as an artefact"
124+
if: ${{ !env.ACT && steps.skip-check.outputs.should_skip != 'true' }}
125+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
126+
with:
127+
name: vulnerabilities-summary.md
128+
path: ./vulnerabilities-summary.md
129+
retention-days: 21
130+
- name: "Fail if Critical or High vulnerabilities found"
131+
if: steps.skip-check.outputs.should_skip != 'true'
132+
shell: bash
133+
run: |
134+
CRITICAL_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Critical") | {id: .vulnerability.id, package: .artifact.name, version: .artifact.version}] | unique_by([.id, .package, .version]) | length' vulnerabilities-repository-report.json)
135+
HIGH_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "High") | {id: .vulnerability.id, package: .artifact.name, version: .artifact.version}] | unique_by([.id, .package, .version]) | length' vulnerabilities-repository-report.json)
136+
echo "Critical: $CRITICAL_COUNT, High: $HIGH_COUNT"
137+
if [[ "$CRITICAL_COUNT" -gt 0 || "$HIGH_COUNT" -gt 0 ]]; then
138+
echo "::error::Found $CRITICAL_COUNT Critical and $HIGH_COUNT High severity vulnerabilities"
139+
exit 1
140+
fi
55141
- name: "Check prerequisites for sending the reports"
142+
if: steps.skip-check.outputs.should_skip != 'true'
56143
shell: bash
57144
id: check
58145
run: echo "secrets_exist=${{ inputs.idp_aws_report_upload_role_name != '' && inputs.idp_aws_report_upload_bucket_endpoint != '' }}" >> $GITHUB_OUTPUT
59146
- name: "Authenticate to send the reports"
60-
if: steps.check.outputs.secrets_exist == 'true'
147+
if: steps.skip-check.outputs.should_skip != 'true' && steps.check.outputs.secrets_exist == 'true'
61148
uses: aws-actions/configure-aws-credentials@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2
62149
with:
63150
role-to-assume: arn:aws:iam::${{ inputs.idp_aws_report_upload_account_id }}:role/${{ inputs.idp_aws_report_upload_role_name }}
64151
aws-region: ${{ inputs.idp_aws_report_upload_region }}
65152
- name: "Send the SBOM and vulnerabilities reports to the central location"
66153
shell: bash
67-
if: steps.check.outputs.secrets_exist == 'true'
154+
if: steps.skip-check.outputs.should_skip != 'true' && steps.check.outputs.secrets_exist == 'true'
68155
run: |
69156
aws s3 cp \
70157
./sbom-repository-report.json.zip \

.github/workflows/stage-1-commit.yaml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -151,21 +151,6 @@ jobs:
151151
uses: asdf-vm/actions/setup@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1
152152
- name: "Lint Terraform"
153153
uses: ./.github/actions/lint-terraform
154-
trivy:
155-
name: "Trivy Scan"
156-
runs-on: ubuntu-latest
157-
timeout-minutes: 5
158-
needs: detect-terraform-changes
159-
if: needs.detect-terraform-changes.outputs.terraform_changed == 'true'
160-
steps:
161-
- name: "Checkout code"
162-
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
163-
- name: "Setup ASDF"
164-
uses: asdf-vm/actions/setup@b7bcd026f18772e44fe1026d729e1611cc435d47 # v4.0.1
165-
- name: "Perform Setup"
166-
uses: ./.github/actions/setup
167-
- name: "Trivy Scan"
168-
uses: ./.github/actions/trivy
169154
count-lines-of-code:
170155
name: "Count lines of code"
171156
runs-on: ubuntu-latest

scripts/reports/create-sbom-report.sh

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
set -euo pipefail
66

7+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
DEFAULT_TOOLING_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
9+
TOOLING_ROOT="${TOOLING_ROOT:-${DEFAULT_TOOLING_ROOT}}"
10+
711
# Script to generate SBOM (Software Bill of Materials) for the repository
812
# content and any artefact created by the CI/CD pipeline. This is a syft command
913
# wrapper. It will run syft natively if it is installed, otherwise it will run
@@ -39,22 +43,23 @@ function create-report() {
3943
function run-syft-natively() {
4044

4145
syft packages dir:"$PWD" \
42-
--config "$PWD/scripts/config/syft.yaml" \
46+
--config "$TOOLING_ROOT/scripts/config/syft.yaml" \
4347
--output spdx-json="$PWD/sbom-repository-report.tmp.json"
4448
}
4549

4650
function run-syft-in-docker() {
4751

4852
# shellcheck disable=SC1091
49-
source ./scripts/docker/docker.lib.sh
53+
source "$TOOLING_ROOT/scripts/docker/docker.lib.sh"
5054

5155
# shellcheck disable=SC2155
5256
local image=$(name=ghcr.io/anchore/syft docker-get-image-version-and-pull)
5357
docker run --rm --platform linux/amd64 \
5458
--volume "$PWD":/workdir \
59+
--volume "$TOOLING_ROOT":/tooling \
5560
"$image" \
5661
packages dir:/workdir \
57-
--config /workdir/scripts/config/syft.yaml \
62+
--config /tooling/scripts/config/syft.yaml \
5863
--output spdx-json=/workdir/sbom-repository-report.tmp.json
5964
}
6065

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/bin/bash
2+
#
3+
# WARNING: This file is managed via the repository template.
4+
# Local changes may diverge from the template source of truth.
5+
#
6+
# Parse vulnerability report JSON and output a human-readable summary
7+
# Usage: ./parse-vulnerabilities.sh <path-to-report.json>
8+
#
9+
10+
set -euo pipefail
11+
12+
if [[ $# -lt 1 ]]; then
13+
echo "Usage: $0 <vulnerability-report.json>"
14+
exit 1
15+
fi
16+
17+
REPORT_FILE="$1"
18+
19+
if [[ ! -f "$REPORT_FILE" ]]; then
20+
echo "Error: File not found: $REPORT_FILE"
21+
exit 1
22+
fi
23+
24+
if ! command -v jq &> /dev/null; then
25+
echo "Error: jq is required but not installed"
26+
exit 1
27+
fi
28+
29+
# Get counts by severity
30+
count_unique_severity() {
31+
local severity="$1"
32+
33+
jq -r --arg sev "$severity" '
34+
[.matches[] | select(.vulnerability.severity == $sev) | {
35+
id: .vulnerability.id,
36+
package: .artifact.name,
37+
version: .artifact.version
38+
}]
39+
| unique_by([.id, .package, .version])
40+
| length
41+
' "$REPORT_FILE"
42+
}
43+
44+
echo "## Vulnerability Report Summary"
45+
echo ""
46+
47+
CRITICAL_COUNT=$(count_unique_severity "Critical")
48+
HIGH_COUNT=$(count_unique_severity "High")
49+
MEDIUM_COUNT=$(count_unique_severity "Medium")
50+
LOW_COUNT=$(count_unique_severity "Low")
51+
TOTAL=$((CRITICAL_COUNT + HIGH_COUNT + MEDIUM_COUNT + LOW_COUNT))
52+
53+
echo "**Total: $TOTAL vulnerabilities** ($CRITICAL_COUNT Critical, $HIGH_COUNT High, $MEDIUM_COUNT Medium, $LOW_COUNT Low)"
54+
echo ""
55+
56+
# Function to print vulnerabilities for a given severity
57+
print_severity_section() {
58+
local severity="$1"
59+
local count="$2"
60+
61+
if [[ "$count" -eq 0 ]]; then
62+
return
63+
fi
64+
65+
echo "### $severity ($count)"
66+
echo ""
67+
echo "| Package | Language | Version | Fix | Description |"
68+
echo "|---------|---------|---------|-----|-------------|"
69+
70+
jq -r --arg sev "$severity" '
71+
[.matches[] | select(.vulnerability.severity == $sev) | {
72+
id: .vulnerability.id,
73+
severity: .vulnerability.severity,
74+
package: .artifact.name,
75+
language: .artifact.language,
76+
version: .artifact.version,
77+
fix: (.vulnerability.fix.versions[0] // "N/A"),
78+
description: .vulnerability.description
79+
}]
80+
| unique_by([.id, .package, .version])
81+
| sort_by(.package)
82+
| .[]
83+
| "| \(.package) | \(.language) | \(.version) | \(.fix) | \(.description[0:70])... |"
84+
' "$REPORT_FILE"
85+
86+
echo ""
87+
}
88+
89+
print_severity_section "Critical" "$CRITICAL_COUNT"
90+
print_severity_section "High" "$HIGH_COUNT"
91+
print_severity_section "Medium" "$MEDIUM_COUNT"
92+
print_severity_section "Low" "$LOW_COUNT"
93+
94+
# Priority packages summary
95+
echo "---"
96+
echo ""
97+
echo "### Priority Packages to Update"
98+
echo ""
99+
100+
jq -r '
101+
[.matches[] | select(.vulnerability.severity == "Critical" or .vulnerability.severity == "High") | .artifact.name]
102+
| unique
103+
| sort
104+
| join(", ")
105+
' "$REPORT_FILE" | fold -s -w 80
106+
107+
echo ""

scripts/reports/scan-vulnerabilities.sh

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
set -euo pipefail
66

7+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
DEFAULT_TOOLING_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
9+
TOOLING_ROOT="${TOOLING_ROOT:-${DEFAULT_TOOLING_ROOT}}"
10+
711
# Script to scan an SBOM file for CVEs (Common Vulnerabilities and Exposures).
812
# This is a grype command wrapper. It will run grype natively if it is
913
# installed, otherwise it will run it in a Docker container.
@@ -42,24 +46,25 @@ function run-grype-natively() {
4246

4347
grype \
4448
sbom:"$PWD/sbom-repository-report.json" \
45-
--config "$PWD/scripts/config/grype.yaml" \
49+
--config "$TOOLING_ROOT/scripts/config/grype.yaml" \
4650
--output json \
4751
--file "$PWD/vulnerabilities-repository-report.tmp.json"
4852
}
4953

5054
function run-grype-in-docker() {
5155

5256
# shellcheck disable=SC1091
53-
source ./scripts/docker/docker.lib.sh
57+
source "$TOOLING_ROOT/scripts/docker/docker.lib.sh"
5458

5559
# shellcheck disable=SC2155
5660
local image=$(name=ghcr.io/anchore/grype docker-get-image-version-and-pull)
5761
docker run --rm --platform linux/amd64 \
5862
--volume "$PWD":/workdir \
63+
--volume "$TOOLING_ROOT":/tooling \
5964
--volume /tmp/grype/db:/.cache/grype/db \
6065
"$image" \
6166
sbom:/workdir/sbom-repository-report.json \
62-
--config /workdir/scripts/config/grype.yaml \
67+
--config /tooling/scripts/config/grype.yaml \
6368
--output json \
6469
--file /workdir/vulnerabilities-repository-report.tmp.json
6570
}

0 commit comments

Comments
 (0)