Skip to content

Commit a07d5c5

Browse files
Merge branch 'main' into feature/CCM-21880_Workflow_Perms_for_Repo_Sync
2 parents 96a2d35 + 6fc87b9 commit a07d5c5

14 files changed

Lines changed: 191 additions & 56 deletions

File tree

.github/actions/build-docs/action.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ inputs:
1616
description: "Additional pnpm workspace filters passed to pnpm install"
1717
required: false
1818
default: ""
19+
GITHUB_TOKEN:
20+
description: "Token for access to github package registry"
21+
required: false
22+
default: ""
1923
runs:
2024
using: "composite"
2125
steps:
@@ -29,6 +33,13 @@ runs:
2933
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
3034
with:
3135
node-version: ${{ inputs.node_version }}
36+
- name: "Configure pnpm for GitHub Packages"
37+
if: ${{ inputs.GITHUB_TOKEN != '' }}
38+
shell: bash
39+
env:
40+
GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }}
41+
run: |
42+
scripts/set-github-token.sh
3243
- name: Install pnpm dependencies
3344
run: pnpm install --frozen-lockfile --filter ./docs ${{ inputs.pnpm_filters }}
3445
shell: bash

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

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ inputs:
2020
description: "IDP AWS report upload endpoint to upload the report to"
2121
required: false
2222
skip_if_pr_has_label:
23-
description: "Skip dependency scanning when the triggering PR has this label"
23+
description: "Skip failing the dependency scan when the triggering PR has this label"
2424
required: false
2525
default: "skip-dependencies-check"
2626
runs:
@@ -68,40 +68,32 @@ runs:
6868
fi
6969
7070
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 }}'."
7671
- name: "Generate SBOM"
77-
if: steps.skip-check.outputs.should_skip != 'true'
7872
shell: bash
7973
run: |
8074
ACTION_ROOT="$(cd "${GITHUB_ACTION_PATH}/../../.." && pwd)"
8175
export TOOLING_ROOT="${ACTION_ROOT}"
8276
export BUILD_DATETIME=${{ inputs.build_datetime }}
8377
"${ACTION_ROOT}/scripts/reports/create-sbom-report.sh"
8478
- name: "Compress SBOM report"
85-
if: steps.skip-check.outputs.should_skip != 'true'
8679
shell: bash
8780
run: zip sbom-repository-report.json.zip sbom-repository-report.json
8881
- name: "Upload SBOM report as an artefact"
89-
if: ${{ !env.ACT && steps.skip-check.outputs.should_skip != 'true' }}
82+
if: ${{ !env.ACT }}
9083
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
9184
with:
9285
name: sbom-repository-report.json.zip
9386
path: ./sbom-repository-report.json.zip
9487
retention-days: 21
9588
- name: "Scan vulnerabilities"
96-
if: steps.skip-check.outputs.should_skip != 'true'
9789
shell: bash
9890
run: |
9991
ACTION_ROOT="$(cd "${GITHUB_ACTION_PATH}/../../.." && pwd)"
10092
export TOOLING_ROOT="${ACTION_ROOT}"
10193
export BUILD_DATETIME=${{ inputs.build_datetime }}
94+
export SCAN_FAIL_ON_SEVERITY=none
10295
"${ACTION_ROOT}/scripts/reports/scan-vulnerabilities.sh"
10396
- name: "Generate vulnerabilities summary"
104-
if: steps.skip-check.outputs.should_skip != 'true'
10597
shell: bash
10698
run: |
10799
ACTION_ROOT="$(cd "${GITHUB_ACTION_PATH}/../../.." && pwd)"
@@ -110,48 +102,49 @@ runs:
110102
cat vulnerabilities-summary.md >> "$GITHUB_STEP_SUMMARY"
111103
fi
112104
- name: "Compress vulnerabilities report"
113-
if: steps.skip-check.outputs.should_skip != 'true'
114105
shell: bash
115106
run: zip vulnerabilities-repository-report.json.zip vulnerabilities-repository-report.json
116107
- name: "Upload vulnerabilities report as an artefact"
117-
if: ${{ !env.ACT && steps.skip-check.outputs.should_skip != 'true' }}
108+
if: ${{ !env.ACT }}
118109
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
119110
with:
120111
name: vulnerabilities-repository-report.json.zip
121112
path: ./vulnerabilities-repository-report.json.zip
122113
retention-days: 21
123114
- name: "Upload vulnerabilities summary as an artefact"
124-
if: ${{ !env.ACT && steps.skip-check.outputs.should_skip != 'true' }}
115+
if: ${{ !env.ACT }}
125116
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
126117
with:
127118
name: vulnerabilities-summary.md
128119
path: ./vulnerabilities-summary.md
129120
retention-days: 21
130121
- name: "Fail if Critical or High vulnerabilities found"
131-
if: steps.skip-check.outputs.should_skip != 'true'
132122
shell: bash
133123
run: |
134124
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)
135125
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)
136126
echo "Critical: $CRITICAL_COUNT, High: $HIGH_COUNT"
137127
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
128+
if [[ "${{ steps.skip-check.outputs.should_skip }}" == "true" ]]; then
129+
echo "Bypassing blocking dependency scan failure because PR has label '${{ inputs.skip_if_pr_has_label }}'."
130+
else
131+
echo "::error::Found $CRITICAL_COUNT Critical and $HIGH_COUNT High severity vulnerabilities"
132+
exit 1
133+
fi
140134
fi
141135
- name: "Check prerequisites for sending the reports"
142-
if: steps.skip-check.outputs.should_skip != 'true'
143136
shell: bash
144137
id: check
145138
run: echo "secrets_exist=${{ inputs.idp_aws_report_upload_role_name != '' && inputs.idp_aws_report_upload_bucket_endpoint != '' }}" >> $GITHUB_OUTPUT
146139
- name: "Authenticate to send the reports"
147-
if: steps.skip-check.outputs.should_skip != 'true' && steps.check.outputs.secrets_exist == 'true'
140+
if: steps.check.outputs.secrets_exist == 'true'
148141
uses: aws-actions/configure-aws-credentials@acca2b1b2070338fb9fd1ca27ecee81d687e58e5 # v6.1.2
149142
with:
150143
role-to-assume: arn:aws:iam::${{ inputs.idp_aws_report_upload_account_id }}:role/${{ inputs.idp_aws_report_upload_role_name }}
151144
aws-region: ${{ inputs.idp_aws_report_upload_region }}
152145
- name: "Send the SBOM and vulnerabilities reports to the central location"
153146
shell: bash
154-
if: steps.skip-check.outputs.should_skip != 'true' && steps.check.outputs.secrets_exist == 'true'
147+
if: steps.check.outputs.secrets_exist == 'true'
155148
run: |
156149
aws s3 cp \
157150
./sbom-repository-report.json.zip \

.pre-commit-hooks.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,10 @@
5151
entry: .github/actions/check-todo-usage/check-todos.sh
5252
language: script
5353
pass_filenames: false
54+
55+
- id: scan-dependencies
56+
name: Scan dependencies
57+
entry: scripts/githooks/scan-dependencies.sh
58+
language: script
59+
pass_filenames: false
60+
stages: [manual]

.tool-versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ terraform-docs 0.19.0
99
trivy 0.69.2
1010
vale 3.6.0
1111
python 3.13.5
12+
ruby 3.2.0
1213

1314
# ==============================================================================
1415
# The section below is reserved for Docker image versions.

docs/github-actions/scan-dependencies.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,50 @@ Scans project dependencies for known vulnerabilities.
1313
### Description
1414

1515
This composite action scans project dependencies to identify known security vulnerabilities.
16+
It always produces the SBOM, raw Grype JSON report, and markdown summary, and it fails the workflow when High or Critical vulnerabilities are present unless the triggering PR carries a configured skip label.
1617

1718
### Usage
1819

1920
```yaml
2021
jobs:
2122
scan-deps:
2223
runs-on: ubuntu-latest
24+
permissions:
25+
contents: read
26+
pull-requests: read
2327
steps:
2428
- uses: actions/checkout@v4
2529

2630
- name: Scan dependencies
2731
uses: NHSDigital/nhs-notify-shared-modules/.github/actions/scan-dependencies@v1.0.0
32+
with:
33+
build_datetime: "${{ needs.metadata.outputs.build_datetime }}"
34+
build_timestamp: "${{ needs.metadata.outputs.build_timestamp }}"
35+
skip_if_pr_has_label: grype-ignore-high-and-critical
2836
```
2937
3038
### Details
3139
3240
- Scans: npm packages, Ruby gems, Python packages
33-
- Checks: Known vulnerabilities (CVEs)
34-
- Reports: Security advisories
35-
- Alerts: High-risk dependencies
41+
- Checks: Known vulnerabilities or CVE findings
42+
- Reports: SBOM JSON, Grype JSON, and markdown summary
43+
- Alerts: High and Critical vulnerabilities fail by default
44+
45+
### Ignore File
46+
47+
If a repository contains a file at `scripts/config/grype.yaml`, each non-comment line is treated as a Grype vulnerability ID to suppress. For example:
48+
49+
```text
50+
CVE-2026-25128 # Ticket to review: CCM-14317
51+
GHSA-xxxx-yyyy-zzzz
52+
```
53+
54+
### Skip Label
55+
56+
Set `skip_if_pr_has_label` to a PR label name if you need to skip the blocking failure for known High or Critical findings that are already being handled elsewhere.
57+
58+
- The scan still runs.
59+
- Reports and summaries are still uploaded.
60+
- Only the final blocking failure is skipped when the label is present.
61+
62+
The default label is `skip-dependencies-check`.

docs/pre-commit-hooks.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ Validates TODO comments follow the required format with Jira ticket IDs.
115115
- id: check-todo-usage
116116
```
117117
118+
#### scan-dependencies
119+
120+
Runs the same SBOM and Grype dependency scan used in CI and prints the markdown vulnerability summary locally.
121+
122+
**Usage:**
123+
124+
```yaml
125+
- repo: https://github.com/NHSDigital/nhs-notify-shared-modules
126+
rev: vX.Y.Z
127+
hooks:
128+
- id: scan-dependencies
129+
```
130+
131+
Run it manually when needed:
132+
133+
```bash
134+
pre-commit run --config scripts/config/pre-commit.yaml --hook-stage manual scan-dependencies
135+
```
136+
118137
## Setup in Your Repository
119138

120139
Add to your `.pre-commit-config.yaml`:
@@ -128,6 +147,7 @@ repos:
128147
- id: check-file-format
129148
- id: check-markdown-format
130149
- id: lint-terraform
150+
- id: scan-dependencies
131151
# Add other hooks as needed
132152
```
133153

@@ -142,3 +162,4 @@ pre-commit install
142162
- All hooks run with `pass_filenames: false` - they operate on the entire repository
143163
- The `scan-secrets` hook checks the entire Git history for security
144164
- Hooks reference scripts in either `scripts/githooks/` or `.github/actions/`
165+
- `scan-dependencies` uses the same Grype-based workflow as CI, respects `scripts/config/grype.yaml` when present, and is configured as a manual-only hook

scripts/config/pre-commit.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,11 @@ repos:
7070
entry: /usr/bin/env check=branch ./.github/actions/check-todo-usage/check-todos.sh
7171
language: script
7272
pass_filenames: false
73+
- repo: local
74+
hooks:
75+
- id: scan-dependencies
76+
name: Scan dependencies
77+
entry: ./scripts/githooks/scan-dependencies.sh
78+
language: script
79+
pass_filenames: false
80+
stages: [manual]

scripts/config/syft.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# - "./out/**/*.json"
55
exclude:
66
- ./.git/**
7+
- ./**/terraform/**/.terraform/**
8+
- ./**/terraform/plugin-cache/**
79

810
# maximum number of workers used to process the list of package catalogers in parallel
911
parallelism: 3
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
TOOLING_ROOT="${TOOLING_ROOT:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
7+
8+
cd "$(git rev-parse --show-toplevel)"
9+
10+
export BUILD_DATETIME="${BUILD_DATETIME:-$(date -u +'%Y-%m-%dT%H:%M:%S%z')}"
11+
export SCAN_FAIL_ON_SEVERITY="${SCAN_FAIL_ON_SEVERITY:-none}"
12+
13+
echo "Step 1: Creating SBOM..."
14+
"${TOOLING_ROOT}/scripts/reports/create-sbom-report.sh" > /dev/null 2>&1
15+
[[ -f sbom-repository-report.json ]] && echo " ✓ SBOM created" || echo " ✗ SBOM not found"
16+
17+
echo "Step 2: Scanning vulnerabilities..."
18+
"${TOOLING_ROOT}/scripts/reports/scan-vulnerabilities.sh"
19+
20+
echo "Step 3: Parsing vulnerabilities..."
21+
REPORT_FILE=""
22+
if [[ -f vulnerabilities-repository-report.json ]]; then
23+
REPORT_FILE="vulnerabilities-repository-report.json"
24+
elif [[ -f vulnerabilities-repository-report.tmp.json ]]; then
25+
REPORT_FILE="vulnerabilities-repository-report.tmp.json"
26+
fi
27+
28+
if [[ -n "$REPORT_FILE" ]]; then
29+
"${TOOLING_ROOT}/scripts/reports/parse-vulnerabilities.sh" "$REPORT_FILE" "true"
30+
31+
# Check if Critical or High vulnerabilities exist and fail
32+
CRITICAL_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "Critical") | {id: .vulnerability.id, package: .artifact.name, version: .artifact.version}] | unique_by([.id, .package, .version]) | length' "$REPORT_FILE")
33+
HIGH_COUNT=$(jq '[.matches[] | select(.vulnerability.severity == "High") | {id: .vulnerability.id, package: .artifact.name, version: .artifact.version}] | unique_by([.id, .package, .version]) | length' "$REPORT_FILE")
34+
35+
if [[ "$CRITICAL_COUNT" -gt 0 || "$HIGH_COUNT" -gt 0 ]]; then
36+
echo ""
37+
echo "❌ Found $CRITICAL_COUNT Critical and $HIGH_COUNT High severity vulnerabilities"
38+
exit 1
39+
fi
40+
else
41+
echo ""
42+
echo "❌ Error: No vulnerability report found"
43+
echo " Expected: vulnerabilities-repository-report.json or vulnerabilities-repository-report.tmp.json"
44+
exit 1
45+
fi

scripts/init.mk

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ githooks-run: # Run git hooks configured in this repository @Operations
4040
--config scripts/config/pre-commit.yaml \
4141
--all-files
4242

43+
githooks-dependencies: # Run git hook for manually scanning depdencies
44+
pre-commit run \
45+
--config scripts/config/pre-commit.yaml \
46+
--hook-stage manual scan-dependencies \
47+
--all-files
48+
4349
_install-dependency: # Install asdf dependency - mandatory: name=[listed in the '.tool-versions' file]; optional: version=[if not listed]
4450
echo ${name}
4551
asdf plugin add ${name} ||:

0 commit comments

Comments
 (0)