Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [ ] I have added tests to cover my changes
- [ ] I have updated the documentation accordingly
- [ ] This PR is a result of pair or mob programming
- [ ] If I have used the 'skip-trivy-package' label I have done so responsibly and in the knowledge that this is being fixed as part of a separate ticket/PR.

---

Expand Down
4 changes: 2 additions & 2 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ If you wish to notify us of a vulnerability via email, please include detailed i

You can reach us at:

- _[ A product team email address ]_
- [cybersecurity@nhs.net](cybersecurity@nhs.net)
- [england.nhsnotify@nhs.net](mailto:england.nhsnotify@nhs.net)
- [cybersecurity@nhs.net](mailto:cybersecurity@nhs.net)

### NCSC

Expand Down
10 changes: 10 additions & 0 deletions .github/actions/check-todo-usage/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: "Check Todo usage"
description: "Check Todo usage"
runs:
using: "composite"
steps:
- name: "Check Todo usage"
shell: bash
run: |
export BRANCH_NAME=origin/${{ github.event.repository.default_branch }}
check=branch ./scripts/githooks/check-todos.sh
2 changes: 1 addition & 1 deletion .github/actions/create-lines-of-code-report/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ runs:
echo "secrets_exist=${{ inputs.idp_aws_report_upload_role_name != '' && inputs.idp_aws_report_upload_bucket_endpoint != '' }}" >> $GITHUB_OUTPUT
- name: "Authenticate to send the report"
if: steps.check.outputs.secrets_exist == 'true'
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ inputs.idp_aws_report_upload_account_id }}:role/${{ inputs.idp_aws_report_upload_role_name }}
aws-region: ${{ inputs.idp_aws_report_upload_region }}
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/scan-dependencies/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ runs:
run: echo "secrets_exist=${{ inputs.idp_aws_report_upload_role_name != '' && inputs.idp_aws_report_upload_bucket_endpoint != '' }}" >> $GITHUB_OUTPUT
- name: "Authenticate to send the reports"
if: steps.check.outputs.secrets_exist == 'true'
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ inputs.idp_aws_report_upload_account_id }}:role/${{ inputs.idp_aws_report_upload_role_name }}
aws-region: ${{ inputs.idp_aws_report_upload_region }}
Expand Down
19 changes: 19 additions & 0 deletions .github/actions/trivy-iac/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: "Trivy IaC Scan"
description: "Scan Terraform IaC using Trivy"
runs:
using: "composite"
steps:
- name: "Trivy Terraform IaC Scan"
shell: bash
run: |
components_exit_code=0
modules_exit_code=0
asdf plugin add trivy || true
asdf install trivy || true
./scripts/terraform/trivy-scan.sh --mode iac ./infrastructure/terraform/components || components_exit_code=$?
./scripts/terraform/trivy-scan.sh --mode iac ./infrastructure/terraform/modules || modules_exit_code=$?

if [ $components_exit_code -ne 0 ] || [ $modules_exit_code -ne 0 ]; then
echo "Trivy misconfigurations detected."
exit 1
fi
17 changes: 17 additions & 0 deletions .github/actions/trivy-package/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: "Trivy Package Scan"
description: "Scan project packages using Trivy"
runs:
using: "composite"
steps:
- name: "Trivy Package Scan"
shell: bash
run: |
exit_code=0
asdf plugin add trivy || true
asdf install trivy || true
./scripts/terraform/trivy-scan.sh --mode package . || exit_code=$?

if [ $exit_code -ne 0 ]; then
echo "Trivy has detected package vulnerablilites. Please refer to https://nhsd-confluence.digital.nhs.uk/spaces/RIS/pages/1257636917/PLAT-KOP-012+-+Trivy+Pipeline+Vulnerability+Scanning+Exemption"
exit 1
fi
305 changes: 305 additions & 0 deletions .github/scripts/dispatch_internal_repo_workflow.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
#!/bin/bash

# Triggers a remote GitHub workflow in nhs-notify-internal and waits for completion.

# Usage:
# ./dispatch_internal_repo_workflow.sh \
# --infraRepoName <repo> \
# --releaseVersion <version> \
# --targetWorkflow <workflow.yaml> \
# --targetEnvironment <env> \
# --targetComponent <component> \
# --targetAccountGroup <group> \
# --terraformAction <action> \
# --internalRef <ref> \
# --overrides <overrides> \
# --overrideProjectName <name> \
# --overrideRoleName <name>

#
# All arguments are required except terraformAction, and internalRef.
# Example:
# ./dispatch_internal_repo_workflow.sh \
# --infraRepoName "nhs-notify-dns" \
# --releaseVersion "v1.2.3" \
# --targetWorkflow "deploy.yaml" \
# --targetEnvironment "prod" \
# --targetComponent "web" \
# --targetAccountGroup "core" \
# --terraformAction "apply" \
# --internalRef "main" \
# --overrides "tf_var=someString" \
# --overrideProjectName nhs \
# --overrideRoleName nhs-service-iam-role

set -e

while [[ $# -gt 0 ]]; do
case $1 in
--infraRepoName) # Name of the infrastructure repo in NHSDigital org (required)
infraRepoName="$2"
shift 2
;;
--releaseVersion) # Release version, commit, or tag to deploy (required)
releaseVersion="$2"
shift 2
;;
--targetWorkflow) # Name of the workflow file to call in nhs-notify-internal (required)
targetWorkflow="$2"
shift 2
;;
--targetEnvironment) # Terraform environment to deploy (required)
targetEnvironment="$2"
shift 2
;;
--targetComponent) # Terraform component to deploy (required)
targetComponent="$2"
shift 2
;;
--targetAccountGroup) # Terraform account group to deploy (required)
targetAccountGroup="$2"
shift 2
;;
--terraformAction) # Terraform action to run (optional)
terraformAction="$2"
shift 2
;;
--internalRef) # Internal repo reference branch or tag (optional, default: "main")
internalRef="$2"
shift 2
;;
--overrides) # Terraform overrides for passing in extra variables (optional)
overrides="$2"
shift 2
;;
--overrideProjectName) # Override the project name (optional)
overrideProjectName="$2"
shift 2
;;
--overrideRoleName) # Override the role name (optional)
overrideRoleName="$2"
shift 2
;;
*)
echo "[ERROR] Unknown argument: $1"
exit 1
;;
esac
done

if [[ -z "$APP_PEM_FILE" ]]; then
echo "[ERROR] PEM_FILE environment variable is not set or is empty."
exit 1
fi

if [[ -z "$APP_CLIENT_ID" ]]; then
echo "[ERROR] CLIENT_ID environment variable is not set or is empty."
exit 1
fi

now=$(date +%s)
iat=$((${now} - 60)) # Issues 60 seconds in the past
exp=$((${now} + 600)) # Expires 10 minutes in the future

b64enc() { openssl base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n'; }

header_json='{
"typ":"JWT",
"alg":"RS256"
}'
# Header encode
header=$( echo -n "${header_json}" | b64enc )

payload_json="{
\"iat\":${iat},
\"exp\":${exp},
\"iss\":\"${APP_CLIENT_ID}\"
}"
# Payload encode
payload=$( echo -n "${payload_json}" | b64enc )

# Signature
header_payload="${header}"."${payload}"
signature=$(
openssl dgst -sha256 -sign <(echo -n "${APP_PEM_FILE}") \
<(echo -n "${header_payload}") | b64enc
)

# Create JWT
JWT="${header_payload}"."${signature}"

INSTALLATION_ID=$(curl -X GET \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${JWT}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
--url "https://api.github.com/app/installations" | jq -r '.[0].id')

PR_TRIGGER_PAT=$(curl --request POST \
--url "https://api.github.com/app/installations/${INSTALLATION_ID}/access_tokens" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${JWT}" \
-H "X-GitHub-Api-Version: 2022-11-28" | jq -r '.token')

# Set default values if not provided
if [[ -z "$PR_TRIGGER_PAT" ]]; then
echo "[ERROR] PR_TRIGGER_PAT environment variable is not set or is empty."
exit 1
fi

if [[ -z "$overrides" ]]; then
overrides=""
fi

if [[ -z "$internalRef" ]]; then
internalRef="main"
fi

echo "==================== Workflow Dispatch Parameters ===================="
echo " infraRepoName: $infraRepoName"
echo " releaseVersion: $releaseVersion"
echo " targetWorkflow: $targetWorkflow"
echo " targetEnvironment: $targetEnvironment"
echo " targetComponent: $targetComponent"
echo " targetAccountGroup: $targetAccountGroup"
echo " terraformAction: $terraformAction"
echo " internalRef: $internalRef"
echo " overrides: $overrides"
echo " overrideProjectName: $overrideProjectName"
echo " overrideRoleName: $overrideRoleName"
echo " targetProject: $targetProject"

DISPATCH_EVENT=$(jq -ncM \
--arg infraRepoName "$infraRepoName" \
--arg releaseVersion "$releaseVersion" \
--arg targetEnvironment "$targetEnvironment" \
--arg targetAccountGroup "$targetAccountGroup" \
--arg targetComponent "$targetComponent" \
--arg terraformAction "$terraformAction" \
--arg targetWorkflow "$targetWorkflow" \
--arg overrides "$overrides" \
--arg overrideProjectName "$overrideProjectName" \
--arg overrideRoleName "$overrideRoleName" \
--arg targetProject "$targetProject" \
'{
"ref": "'"$internalRef"'",
"inputs": (
(if $infraRepoName != "" then { "infraRepoName": $infraRepoName } else {} end) +
(if $terraformAction != "" then { "terraformAction": $terraformAction } else {} end) +
(if $overrideProjectName != "" then { "overrideProjectName": $overrideProjectName } else {} end) +
(if $overrideRoleName != "" then { "overrideRoleName": $overrideRoleName } else {} end) +
(if $targetProject != "" then { "targetProject": $targetProject } else {} end) +
{
"releaseVersion": $releaseVersion,
"targetEnvironment": $targetEnvironment,
"targetAccountGroup": $targetAccountGroup,
"targetComponent": $targetComponent,
"overrides": $overrides,
}
)
}')

echo "[INFO] Triggering workflow '$targetWorkflow' in nhs-notify-internal..."

trigger_response=$(curl -s -L \
--fail \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${PR_TRIGGER_PAT}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/NHSDigital/nhs-notify-internal/actions/workflows/$targetWorkflow/dispatches" \
-d "$DISPATCH_EVENT" 2>&1)

if [[ $? -ne 0 ]]; then
echo "[ERROR] Failed to trigger workflow. Response: $trigger_response"
exit 1
fi

echo "[INFO] Workflow trigger request sent successfully, waiting for completion..."

sleep 10 # Wait a few seconds before checking for the presence of the api to account for GitHub updating

# Poll GitHub API to check the workflow status
workflow_run_url=""

for _ in {1..18}; do

response=$(curl -s -L \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${PR_TRIGGER_PAT}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/NHSDigital/nhs-notify-internal/actions/runs?event=workflow_dispatch")

if ! echo "$response" | jq empty 2>/dev/null; then
echo "[ERROR] Invalid JSON response from GitHub API during workflow polling:"
echo "$response"
exit 1
fi

workflow_run_url=$(echo "$response" | jq -r \
--arg targetWorkflow "$targetWorkflow" \
--arg targetEnvironment "$targetEnvironment" \
--arg targetAccountGroup "$targetAccountGroup" \
--arg targetComponent "$targetComponent" \
--arg terraformAction "$terraformAction" \
'.workflow_runs[]
| select(.path == ".github/workflows/" + $targetWorkflow)
| select(.name
| contains($targetEnvironment)
and contains($targetAccountGroup)
and contains($targetComponent)
and contains($terraformAction)
)
| .url')

if [[ -n "$workflow_run_url" && "$workflow_run_url" != null ]]; then
# Workflow_run_url is a list of all workflows which were run for this combination of inputs, but are the API uri
workflow_run_url=$(echo "$workflow_run_url" | head -n 1)

# Take the first and strip it back to being an accessible url
# Example https://api.github.com/repos/MyOrg/my-repo/actions/runs/12346789 becomes
# becomes https://github.com/MyOrg/my-repo/actions/runs/12346789
workflow_run_ui_url=${workflow_run_url/api./} # Strips the api. prefix
workflow_run_ui_url=${workflow_run_ui_url/\/repos/} # Strips the repos/ uri
echo "[INFO] Found workflow run url: $workflow_run_ui_url"
break
fi

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Waiting for workflow to start..."
sleep 10
done

if [[ -z "$workflow_run_url" || "$workflow_run_url" == null ]]; then
echo "[ERROR] Failed to get the workflow run url. Exiting."
exit 1
fi

# Wait for workflow completion
while true; do
sleep 10
response=$(curl -s -L \
-H "Authorization: Bearer ${PR_TRIGGER_PAT}" \
-H "Accept: application/vnd.github+json" \
"$workflow_run_url")

status=$(echo "$response" | jq -r '.status')
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Workflow status: $status"

if [ "$status" == "completed" ]; then
conclusion=$(echo "$response" | jq -r '.conclusion')
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Workflow conclusion: $conclusion"

if [ -z "$conclusion" ] || [ "$conclusion" == "null" ]; then
echo "[WARN] Workflow marked completed but conclusion not yet available, retrying..."
sleep 5
continue
fi

if [ "$conclusion" == "success" ]; then
echo "[SUCCESS] Workflow completed successfully!"
exit 0
else
echo "[FAIL] Workflow failed with conclusion: $conclusion"
exit 1
fi
fi
done
Loading