diff --git a/.github/actions/trivy-iac/action.yaml b/.github/actions/trivy-iac/action.yaml new file mode 100644 index 000000000..583f93566 --- /dev/null +++ b/.github/actions/trivy-iac/action.yaml @@ -0,0 +1,18 @@ +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 + + ./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 diff --git a/.github/actions/trivy-package/action.yaml b/.github/actions/trivy-package/action.yaml new file mode 100644 index 000000000..d6ee4a3f8 --- /dev/null +++ b/.github/actions/trivy-package/action.yaml @@ -0,0 +1,16 @@ +name: "Trivy Package Scan" +description: "Scan project packages using Trivy" +runs: + using: "composite" + steps: + - name: "Trivy Package Scan" + shell: bash + run: | + exit_code=0 + + ./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 diff --git a/.github/actions/trivy/action.yaml b/.github/actions/trivy/action.yaml deleted file mode 100644 index be940ce56..000000000 --- a/.github/actions/trivy/action.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Trivy Scan" -runs: - using: "composite" - steps: - - name: "Trivy Terraform IAC Scan" - shell: bash - run: | - components_exit_code=0 - modules_exit_code=0 - - ./scripts/terraform/trivy.sh ./infrastructure/terraform/components || components_exit_code=$? - ./scripts/terraform/trivy.sh ./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 diff --git a/.github/workflows/stage-1-commit.yaml b/.github/workflows/stage-1-commit.yaml index fc3b37346..55f330aa3 100644 --- a/.github/workflows/stage-1-commit.yaml +++ b/.github/workflows/stage-1-commit.yaml @@ -146,21 +146,38 @@ jobs: uses: actions/checkout@v5.0.0 - name: "Lint Terraform" uses: ./.github/actions/lint-terraform - trivy: - name: "Trivy Scan" + trivy-iac: + name: "Trivy IaC Scan" + permissions: + contents: read runs-on: ubuntu-latest timeout-minutes: 10 needs: detect-terraform-changes if: needs.detect-terraform-changes.outputs.terraform_changed == 'true' steps: - name: "Checkout code" - uses: actions/checkout@v5.0.0 + uses: actions/checkout@v4 + - name: "Setup ASDF" + uses: asdf-vm/actions/setup@1902764435ca0dd2f3388eea723a4f92a4eb8302 + - name: "Perform Setup" + uses: ./.github/actions/setup + - name: "Trivy IaC Scan" + uses: ./.github/actions/trivy-iac + trivy-package: + name: "Trivy Package Scan" + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: "Checkout code" + uses: actions/checkout@v4 - name: "Setup ASDF" - uses: asdf-vm/actions/setup@v4 + uses: asdf-vm/actions/setup@1902764435ca0dd2f3388eea723a4f92a4eb8302 - name: "Perform Setup" uses: ./.github/actions/setup - - name: "Trivy Scan" - uses: ./.github/actions/trivy + - name: "Trivy Package Scan" + uses: ./.github/actions/trivy-package count-lines-of-code: name: "Count lines of code" runs-on: ubuntu-latest diff --git a/.tool-versions b/.tool-versions index 9f69be6f1..7dbf3ab68 100644 --- a/.tool-versions +++ b/.tool-versions @@ -13,8 +13,8 @@ python 3.13.2 # The section below is reserved for Docker image versions. # TODO: Move this section - consider using a different file for the repository template dependencies. -# docker/ghcr.io/anchore/grype v0.92.2@sha256:651e558f9ba84f2a790b3449c8a57cbbf4f34e004f7d3f14ae8f8cbeede4cd33 # SEE: https://github.com/anchore/grype/pkgs/container/grype -# docker/ghcr.io/anchore/syft v1.26.0@sha256:de078f51704a213906970b1475edd6006b8af50aa159852e125518237487b8c6 # SEE: https://github.com/anchore/syft/pkgs/container/syft +# docker/ghcr.io/anchore/grype v0.104.3@sha256:d340f4f8b3b7e6e72a6c9c0152f25402ed8a2d7375dba1dfce4e53115242feb6 # SEE: https://github.com/anchore/grype/pkgs/container/grype +# docker/ghcr.io/anchore/syft v1.39.0@sha256:6f13bb010923c33fb197047c8f88888e77071bd32596b3f605d62a133e493ce4 # SEE: https://github.com/anchore/syft/pkgs/container/syft # docker/ghcr.io/gitleaks/gitleaks v8.24.0@sha256:2bcceac45179b3a91bff11a824d0fb952585b429e54fc928728b1d4d5c3e5176 # SEE: https://github.com/gitleaks/gitleaks/pkgs/container/gitleaks # docker/ghcr.io/igorshubovych/markdownlint-cli v0.37.0@sha256:fb3e79946fce78e1cde84d6798c6c2a55f2de11fc16606a40d49411e281d950d # SEE: https://github.com/igorshubovych/markdownlint-cli/pkgs/container/markdownlint-cli # docker/ghcr.io/make-ops-tools/gocloc latest@sha256:6888e62e9ae693c4ebcfed9f1d86c70fd083868acb8815fe44b561b9a73b5032 # SEE: https://github.com/make-ops-tools/gocloc/pkgs/container/gocloc diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 000000000..5d07d59af --- /dev/null +++ b/.trivyignore @@ -0,0 +1 @@ +CVE-2025-68154 # Impacts Windows systems only. https://nvd.nist.gov/vuln/detail/CVE-2025-68154 Ticket to review: https://nhsd-jira.digital.nhs.uk/browse/CCM-13645 diff --git a/scripts/config/trivy.yaml b/scripts/config/trivy.yaml index a4eff466e..29707dbba 100644 --- a/scripts/config/trivy.yaml +++ b/scripts/config/trivy.yaml @@ -4,3 +4,4 @@ exit-code: 1 # When issues are found scan: skip-files: - "**/.terraform/**/*" + - "**/node_modules/**/*" diff --git a/scripts/terraform/trivy-scan.sh b/scripts/terraform/trivy-scan.sh new file mode 100755 index 000000000..15656233f --- /dev/null +++ b/scripts/terraform/trivy-scan.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash + +# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/NHSDigital/nhs-notify-repository-template). Raise a PR instead. + +set -euo pipefail + +function usage() { + cat <<'EOF' +Usage: ./scripts/terraform/trivy-scan.sh --mode [directory] + +Options: + --mode, -m Scan type to run. Accepts "iac" or "package" (required). + --help, -h Show this message. + [directory] Directory to scan. Defaults to the repository root. + +Environment variables: + FORCE_USE_DOCKER=true Force execution through Docker even if Trivy is installed locally. + VERBOSE=true Enable bash -x tracing. +EOF +} + +function main() { + cd "$(git rev-parse --show-toplevel)" + + local scan_mode="" + local dir_to_scan="." + + while [[ $# -gt 0 ]]; do + case "$1" in + --mode|-m) + if [[ $# -lt 2 ]]; then + echo "Error: --mode requires an argument." >&2 + usage + exit 1 + fi + scan_mode="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + --) + shift + break + ;; + -*) + echo "Unknown option: $1" >&2 + usage + exit 1 + ;; + *) + dir_to_scan="$1" + shift + ;; + esac + done + + if [[ $# -gt 0 ]]; then + dir_to_scan="$1" + fi + + if [[ -z "$scan_mode" ]]; then + echo "Error: --mode must be provided (iac|package)." >&2 + usage + exit 1 + fi + + case "$scan_mode" in + iac|package) + ;; + *) + echo "Error: unknown mode '$scan_mode'. Expected 'iac' or 'package'." >&2 + usage + exit 1 + ;; + esac + + if command -v trivy > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then + run-trivy-natively "$scan_mode" "$dir_to_scan" + else + run-trivy-in-docker "$scan_mode" "$dir_to_scan" + fi +} + +function run-trivy-natively() { + local scan_mode="$1" + local dir_to_scan="$2" + + echo "Trivy found locally, running natively" + echo "Running Trivy ($scan_mode) on directory: $dir_to_scan" + + if execute-trivy-command "$scan_mode" "$dir_to_scan"; then + check-trivy-status 0 + else + local status=$? + check-trivy-status "$status" + fi +} + +function run-trivy-in-docker() { + # shellcheck disable=SC1091 + source ./scripts/docker/docker.lib.sh + + local scan_mode="$1" + local dir_to_scan="$2" + + # shellcheck disable=SC2155 + local image=$(name=aquasec/trivy docker-get-image-version-and-pull) + + echo "Trivy not found locally, running in Docker Container" + echo "Running Trivy ($scan_mode) on directory: $dir_to_scan" + + if execute-trivy-in-docker "$image" "$scan_mode" "$dir_to_scan"; then + check-trivy-status 0 + else + local status=$? + check-trivy-status "$status" + fi +} + +function execute-trivy-command() { + local scan_mode="$1" + local dir_to_scan="$2" + + if [[ "$scan_mode" == "iac" ]]; then + trivy config \ + --config scripts/config/trivy.yaml \ + --tf-exclude-downloaded-modules \ + "$dir_to_scan" + else + trivy \ + --config scripts/config/trivy.yaml \ + fs "$dir_to_scan" \ + --scanners vuln \ + --severity HIGH,CRITICAL \ + --include-dev-deps + fi +} + +function execute-trivy-in-docker() { + local image="$1" + local scan_mode="$2" + local dir_to_scan="$3" + + if [[ "$scan_mode" == "iac" ]]; then + docker run --rm --platform linux/amd64 \ + --volume "$PWD":/workdir \ + --workdir /workdir \ + "$image" \ + config \ + --config scripts/config/trivy.yaml \ + --tf-exclude-downloaded-modules \ + "$dir_to_scan" + else + docker run --rm --platform linux/amd64 \ + --volume "$PWD":/workdir \ + --workdir /workdir \ + "$image" \ + --config scripts/config/trivy.yaml \ + fs "$dir_to_scan" \ + --scanners vuln \ + --severity HIGH,CRITICAL \ + --include-dev-deps + fi +} + +function check-trivy-status() { + local status="$1" + + if [[ "$status" -eq 0 ]]; then + echo "Trivy completed successfully." + return 0 + fi + + echo "Trivy found issues." + exit "$status" +} + +function is-arg-true() { + if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then + return 0 + else + return 1 + fi +} + +# ============================================================================== + +is-arg-true "${VERBOSE:-false}" && set -x + +main "$@" + +exit 0 diff --git a/scripts/terraform/trivy.sh b/scripts/terraform/trivy.sh deleted file mode 100755 index 93caabd8d..000000000 --- a/scripts/terraform/trivy.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/NHSDigital/nhs-notify-repository-template). Raise a PR instead. - -set -euo pipefail - -# TFSec command wrapper. It will run the command natively if TFSec is -# installed, otherwise it will run it in a Docker container. -# Run tfsec for security checks on Terraform code. -# -# Usage: -# $ ./trivy.sh [directory] -# ============================================================================== - -function main() { - - cd "$(git rev-parse --show-toplevel)" - - local dir_to_scan=${1:-.} - - if command -v trivy > /dev/null 2>&1 && ! is-arg-true "${FORCE_USE_DOCKER:-false}"; then - # shellcheck disable=SC2154 - run-trivy-natively "$dir_to_scan" - else - run-trivy-in-docker "$dir_to_scan" - fi -} - -# Run trivy on the specified directory. -# Arguments: -# $1 - Directory to scan -function run-trivy-natively() { - - local dir_to_scan="$1" - - echo "Trivy found locally, running natively" - - echo "Running Trivy on directory: $dir_to_scan" - trivy config \ - --config scripts/config/trivy.yaml \ - --tf-exclude-downloaded-modules \ - "${dir_to_scan}" - - check-trivy-status -} - -# Check the exit status of tfsec. -function check-trivy-status() { - - if [ $? -eq 0 ]; then - echo "Trivy completed successfully." - else - echo "Trivy found issues." - exit 1 - fi -} - -function run-trivy-in-docker() { - - # shellcheck disable=SC1091 - source ./scripts/docker/docker.lib.sh - local dir_to_scan="$1" - - # shellcheck disable=SC2155 - local image=$(name=aquasec/trivy docker-get-image-version-and-pull) - # shellcheck disable=SC2086 - echo "Trivy not found locally, running in Docker Container" - echo "Running Trivy on directory: $dir_to_scan" - docker run --rm --platform linux/amd64 \ - --volume "$PWD":/workdir \ - --workdir /workdir \ - "$image" \ - config \ - --config scripts/config/trivy.yaml \ - --tf-exclude-downloaded-modules \ - "${dir_to_scan}" - check-trivy-status -} -# ============================================================================== - -function is-arg-true() { - - if [[ "$1" =~ ^(true|yes|y|on|1|TRUE|YES|Y|ON)$ ]]; then - return 0 - else - return 1 - fi -} - -# ============================================================================== - -is-arg-true "${VERBOSE:-false}" && set -x - -main "$@" - -exit 0