diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index de89ecea6..92089de88 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -91,13 +91,17 @@ jobs: playwright-${{ runner.os }}- - name: "Install Playwright browsers" + timeout-minutes: 3 working-directory: tests run: npx playwright install --with-deps - name: "Start the application" if: env.TARGET_ENV == 'local' run: | - npm run start + npm run local:start + env: + BUILDKIT_PROGRESS: plain # or "quiet" to fully suppress build output + DOCKER_CLI_HINTS: false # removes "What's next?" hints - name: "Show application status" if: env.TARGET_ENV == 'local' @@ -179,6 +183,34 @@ jobs: docker compose -f local-environment/docker-compose.yml logs "$service" > "tests/testResults/docker-compose-${service}.log" 2>&1 done + - name: "Detect flaky tests" + if: always() + run: | + RESULTS_FILE="tests/testResults/test-results.json" + if [ ! -f "$RESULTS_FILE" ]; then + echo "No test results JSON found, skipping flaky detection" + exit 0 + fi + + # Check top-level stats for flaky count first + FLAKY_COUNT=$(jq '.stats.flaky // 0' "$RESULTS_FILE" 2>/dev/null || echo 0) + echo "Flaky test count: $FLAKY_COUNT" + + if [ "$FLAKY_COUNT" -gt 0 ]; then + # Extract flaky test details: specs contain tests, status is on test objects + FLAKY_TESTS=$(jq -r ' + [.. | .specs?[]? | {title: .title, file: .file, line: .line, tests: [.tests[]? | select(.status == "flaky") | .projectName]} | select(.tests | length > 0)] + | .[] | "[\(.tests | join(", "))] \(.title) (\(.file):\(.line))" + ' "$RESULTS_FILE" 2>/dev/null) + + echo "::warning::Flaky tests detected (passed on retry):" + while IFS= read -r test; do + echo "::warning:: Flaky: $test" + done <<< "$FLAKY_TESTS" + else + echo "No flaky tests detected" + fi + - name: "Publish Test Results" uses: dorny/test-reporter@v3 if: always() @@ -210,6 +242,22 @@ jobs: echo ":warning: No test results found" >> $GITHUB_STEP_SUMMARY fi + # Append flaky test details to summary + RESULTS_FILE="tests/testResults/test-results.json" + if [ -f "$RESULTS_FILE" ]; then + FLAKY_COUNT=$(jq '.stats.flaky // 0' "$RESULTS_FILE" 2>/dev/null || echo 0) + if [ "$FLAKY_COUNT" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### :warning: Flaky Tests ($FLAKY_COUNT)" >> $GITHUB_STEP_SUMMARY + echo "These tests failed initially but passed on retry:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + jq -r ' + [.. | .specs?[]? | {title: .title, file: .file, line: .line, tests: [.tests[]? | select(.status == "flaky") | .projectName]} | select(.tests | length > 0)] + | .[] | "- **[\(.tests | join(", "))] \(.title)** (\(.file):\(.line))" + ' "$RESULTS_FILE" 2>/dev/null >> $GITHUB_STEP_SUMMARY + fi + fi + - name: "Upload test results" uses: actions/upload-artifact@v7 if: always() diff --git a/.mise.toml b/.mise.toml index cd8f76d25..bf3046c49 100644 --- a/.mise.toml +++ b/.mise.toml @@ -6,10 +6,6 @@ install_before = "7d" [settings.python] compile = false -# Disable to avoid calling Github API -[settings.aqua] -cosign = false - [tools] # Custom registries are not included in mise-versions # https://mise-versions.jdx.dev/ @@ -40,8 +36,11 @@ python = "3.14.2" ## Security scanning -# https://github.com/aquasecurity/trivy/releases -"aqua:aquasecurity/trivy" = "v0.69.3" +# https://github.com/anchore/grype/releases +grype = "0.111.0" + +# https://github.com/google/osv-scanner/releases +osv-scanner = "2.3.5" # https://github.com/gitleaks/gitleaks/releases gitleaks = "8.30.1" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7eb893d9d..92bb38210 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,19 +8,46 @@ repos: args: [--markdown-linebreak-ext=md] - id: end-of-file-fixer - id: check-yaml + args: [--allow-multiple-documents] - id: check-json + - id: check-toml - id: check-added-large-files + args: ["--maxkb=500"] - id: check-case-conflict - id: check-merge-conflict + - id: check-symlinks - id: detect-private-key - id: check-executables-have-shebangs - - id: forbid-submodules + - id: mixed-line-ending + args: [--fix=lf] + - id: no-commit-to-branch + args: [--branch, main, --branch, master, --branch, develop] + + # # https://github.com/google/osv-scanner/releases + # - repo: https://github.com/google/osv-scanner + # rev: v2.3.5 + # hooks: + # - id: osv-scanner - repo: local hooks: - - id: trivy-fs-scan - name: Trivy filesystem scan - entry: trivy fs --scanners vuln --severity HIGH,CRITICAL --exit-code 1 . + # - id: grype-fs-scan + # name: Grype filesystem vulnerability scan + # entry: scripts/grype-scan.sh + # language: script + # pass_filenames: false + # always_run: true + # require_serial: true + + # - id: osv-scanner + # name: OSV-Scanner dependency vulnerability scan + # entry: osv-scanner scan --recursive --format table . + # language: system + # pass_filenames: false + + - id: grype + name: grype + entry: bash -c 'echo $PWD; whereis grype; grype --version; grype "dir:$PWD" --quiet -o template -t $PWD/scripts/config/grype-table.tmpl --name hometest-service -c $PWD/scripts/config/grype.yaml' language: system pass_filenames: false @@ -103,6 +130,22 @@ repos: args: - --args=-recursive + - id: terraform_docs + files: ^local-environment/infra + args: + - --hook-config=--path-to-file=README.md + - --hook-config=--add-to-existing-file=true + - --hook-config=--create-file-if-not-exist=true + - --hook-config=--recursive + - --hook-config=--recursive-path=modules + - --hook-config=--output-file=README.md + - --hook-config=--output-mode=inject + - --hook-config=--output-template='\n{{ .Content }}\n' + - --hook-config=--working-dir=terraform + + - id: terraform_validate + files: ^local-environment/infra + - repo: https://github.com/sqlfluff/sqlfluff rev: 4.1.0 # pin to a specific version hooks: @@ -110,3 +153,8 @@ repos: args: ["--config", ".sqlfluff"] # optional, if you keep config elsewhere - id: sqlfluff-fix args: ["--config", ".sqlfluff"] # auto-fix style issues + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.12 + hooks: + - id: actionlint diff --git a/local-environment/infra/main.tf b/local-environment/infra/main.tf index 6dff99cf4..80154cf18 100644 --- a/local-environment/infra/main.tf +++ b/local-environment/infra/main.tf @@ -89,6 +89,41 @@ locals { resolved_supplier_service_url = var.local_supplier_service_url_override != null ? var.local_supplier_service_url_override : local.wiremock_container_base_url resolved_use_wiremock_auth = local.resolved_nhs_login_override_container_base_url != null ? length(regexall("wiremock", lower(local.resolved_nhs_login_override_container_base_url))) > 0 : local.use_wiremock_mode + + # Common DB environment variables shared across multiple lambdas + common_db_env = { + DB_USERNAME = "app_user" + DB_ADDRESS = "postgres-db" + DB_PORT = "5432" + DB_NAME = "local_hometest_db" + DB_SCHEMA = "hometest" + DB_SECRET_NAME = "postgres-db-password" + DB_SSL = "false" + } + + # Common base env for all lambdas + common_base_env = { + NODE_OPTIONS = "--enable-source-maps" + ALLOW_ORIGIN = "http://localhost:3000" + } + + # Common CORS settings shared across multiple lambdas + common_cors = { + enable_cors = true + cors_allow_origin = "http://localhost:3000" + cors_allow_credentials = true + } + + # Common lambda module parameters + common_lambda_params = { + project_name = var.project_name + lambda_role_arn = aws_iam_role.lambda_role.arn + environment = var.environment + aws_region = var.aws_region + api_gateway_id = aws_api_gateway_rest_api.api.id + api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id + api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn + } } # Fail early if required secrets are missing @@ -186,63 +221,51 @@ resource "aws_api_gateway_rest_api" "api" { module "eligibility_lookup_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "eligibility-lookup-lambda" - zip_path = "${path.module}/../../lambdas/dist/eligibility-lookup-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "eligibility-lookup" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - http_method = "GET" - - enable_cors = true - cors_allow_origin = "http://localhost:3000" + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "eligibility-lookup-lambda" + zip_path = "${path.module}/../../lambdas/dist/eligibility-lookup-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "eligibility-lookup" + http_method = "GET" + + enable_cors = local.common_cors.enable_cors + cors_allow_origin = local.common_cors.cors_allow_origin cors_allow_methods = ["GET", "OPTIONS"] cors_allow_headers = ["Content-Type", "Authorization"] - cors_allow_credentials = true + cors_allow_credentials = local.common_cors.cors_allow_credentials - environment_variables = { - NODE_OPTIONS = "--enable-source-maps" - ALLOW_ORIGIN = "http://localhost:3000" - DB_USERNAME = "app_user" - DB_ADDRESS = "postgres-db" - DB_PORT = "5432" - DB_NAME = "local_hometest_db" - DB_SCHEMA = "hometest" - DB_SECRET_NAME = "postgres-db-password" - DB_SSL = "false" - } + environment_variables = merge(local.common_base_env, local.common_db_env) } # Login Lambda module "login_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "login" - zip_path = "${path.module}/../../lambdas/dist/login-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "login" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - http_method = "POST" - timeout = 30 - - enable_cors = true - cors_allow_origin = "http://localhost:3000" + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "login" + zip_path = "${path.module}/../../lambdas/dist/login-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "login" + http_method = "POST" + timeout = 30 + + enable_cors = local.common_cors.enable_cors + cors_allow_origin = local.common_cors.cors_allow_origin cors_allow_methods = ["POST", "OPTIONS"] cors_allow_headers = ["Content-Type", "Authorization"] - cors_allow_credentials = true + cors_allow_credentials = local.common_cors.cors_allow_credentials - environment_variables = { - NODE_OPTIONS = "--enable-source-maps", - ALLOW_ORIGIN = "http://localhost:3000", + environment_variables = merge(local.common_base_env, { NHS_LOGIN_BASE_ENDPOINT_URL = local.resolved_nhs_login_base_url, NHS_LOGIN_CLIENT_ID = "hometest", NHS_LOGIN_REDIRECT_URL = "http://localhost:3000/callback", @@ -252,58 +275,54 @@ module "login_lambda" { AUTH_REFRESH_TOKEN_EXPIRY_DURATION_MINUTES = "60", AUTH_COOKIE_SAME_SITE = "Lax" AUTH_COOKIE_SECURE = "false" - } + }) } module "session_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "session" - zip_path = "${path.module}/../../lambdas/dist/session-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "session" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - http_method = "GET" - - enable_cors = true - cors_allow_origin = "http://localhost:3000" + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "session" + zip_path = "${path.module}/../../lambdas/dist/session-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "session" + http_method = "GET" + + enable_cors = local.common_cors.enable_cors + cors_allow_origin = local.common_cors.cors_allow_origin cors_allow_methods = ["GET", "OPTIONS"] cors_allow_headers = ["Content-Type", "Authorization"] - cors_allow_credentials = true + cors_allow_credentials = local.common_cors.cors_allow_credentials - environment_variables = { - NODE_OPTIONS = "--enable-source-maps" - ALLOW_ORIGIN = "http://localhost:3000" + environment_variables = merge(local.common_base_env, { AUTH_COOKIE_KEY_ID = "key" AUTH_COOKIE_PUBLIC_KEY_SECRET_NAME = "nhs-login-private-key" NHS_LOGIN_BASE_ENDPOINT_URL = local.resolved_nhs_login_base_url, - } + }) } # Postcode Lookup Lambda module "postcode_lookup_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "postcode-lookup" - zip_path = "${path.module}/../../lambdas/dist/postcode-lookup-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "postcode-lookup" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - http_method = "GET" - - environment_variables = { - NODE_OPTIONS = "--enable-source-maps", - ALLOW_ORIGIN = "http://localhost:3000", + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "postcode-lookup" + zip_path = "${path.module}/../../lambdas/dist/postcode-lookup-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "postcode-lookup" + http_method = "GET" + + environment_variables = merge(local.common_base_env, { POSTCODE_LOOKUP_CREDENTIALS_SECRET_NAME = "os-places-creds", POSTCODE_LOOKUP_BASE_URL = local.resolved_postcode_lookup_base_url, POSTCODE_LOOKUP_TIMEOUT_MS = "5000", @@ -311,22 +330,22 @@ module "postcode_lookup_lambda" { POSTCODE_LOOKUP_RETRY_DELAY_MS = "1000", POSTCODE_LOOKUP_RETRY_BACKOFF_FACTOR = "2", USE_STUB_POSTCODE_CLIENT = false, - } + }) } module "hello_world_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "hello-world" - zip_path = "${path.module}/../../lambdas/dist/hello-world-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "hello-world" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "hello-world" + zip_path = "${path.module}/../../lambdas/dist/hello-world-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "hello-world" environment_variables = { NODE_OPTIONS = "--enable-source-maps" @@ -344,27 +363,20 @@ resource "aws_sqs_queue" "notify_messages" { module "order_router_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "order-router" - zip_path = "${path.module}/../../lambdas/dist/order-router-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "test-order/order" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - - environment_variables = { - NODE_OPTIONS = "--enable-source-maps" - DB_USERNAME = "app_user" - DB_ADDRESS = "postgres-db" - DB_PORT = "5432" - DB_NAME = "local_hometest_db" - DB_SCHEMA = "hometest" - DB_SECRET_NAME = "postgres-db-password" - DB_SSL = "false" - } + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "order-router" + zip_path = "${path.module}/../../lambdas/dist/order-router-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "test-order/order" + + environment_variables = merge(local.common_db_env, { + NODE_OPTIONS = "--enable-source-maps" + }) } resource "aws_lambda_event_source_mapping" "order_router_order_placement" { @@ -377,161 +389,114 @@ resource "aws_lambda_event_source_mapping" "order_router_order_placement" { module "order_result_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "order-result" - zip_path = "${path.module}/../../lambdas/dist/order-result-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "result" - http_method = "POST" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - - environment_variables = { - NODE_OPTIONS = "--enable-source-maps" - ALLOW_ORIGIN = "http://localhost:3000" - DB_USERNAME = "app_user" - DB_ADDRESS = "postgres-db" - DB_PORT = "5432" - DB_NAME = "local_hometest_db" - DB_SCHEMA = "hometest" - DB_SECRET_NAME = "postgres-db-password" - DB_SSL = "false" + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "order-result" + zip_path = "${path.module}/../../lambdas/dist/order-result-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "result" + http_method = "POST" + + environment_variables = merge(local.common_base_env, local.common_db_env, { NOTIFY_MESSAGES_QUEUE_URL = aws_sqs_queue.notify_messages.url HOME_TEST_BASE_URL = "http://localhost:3000" - } + }) } module "order_service_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "order-service" - zip_path = "${path.module}/../../lambdas/dist/order-service-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "order" - http_method = "POST" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - - enable_cors = true - cors_allow_origin = "http://localhost:3000" + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "order-service" + zip_path = "${path.module}/../../lambdas/dist/order-service-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "order" + http_method = "POST" + + enable_cors = local.common_cors.enable_cors + cors_allow_origin = local.common_cors.cors_allow_origin cors_allow_methods = ["POST", "OPTIONS"] cors_allow_headers = ["Content-Type", "Authorization", "X-Correlation-ID"] - cors_allow_credentials = true + cors_allow_credentials = local.common_cors.cors_allow_credentials - environment_variables = { - NODE_OPTIONS = "--enable-source-maps" + environment_variables = merge(local.common_base_env, local.common_db_env, { ORDER_PLACEMENT_QUEUE_URL = aws_sqs_queue.order_placement.url - ALLOW_ORIGIN = "http://localhost:3000" - DB_USERNAME = "app_user" - DB_ADDRESS = "postgres-db" - DB_PORT = "5432" - DB_NAME = "local_hometest_db" - DB_SCHEMA = "hometest" - DB_SECRET_NAME = "postgres-db-password" - DB_SSL = "false" - } + }) } module "get_order_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "get-order" - zip_path = "${path.module}/../../lambdas/dist/get-order-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "get-order" - http_method = "GET" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - - enable_cors = true - cors_allow_origin = "http://localhost:3000" + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "get-order" + zip_path = "${path.module}/../../lambdas/dist/get-order-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "get-order" + http_method = "GET" + + enable_cors = local.common_cors.enable_cors + cors_allow_origin = local.common_cors.cors_allow_origin cors_allow_methods = ["GET", "OPTIONS"] - environment_variables = { - NODE_OPTIONS = "--enable-source-maps" - ALLOW_ORIGIN = "http://localhost:3000" - DB_USERNAME = "app_user" - DB_ADDRESS = "postgres-db" - DB_PORT = "5432" - DB_NAME = "local_hometest_db" - DB_SCHEMA = "hometest" - DB_SECRET_NAME = "postgres-db-password" - DB_SSL = "false" - } + environment_variables = merge(local.common_base_env, local.common_db_env) } module "get_results_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "get-results" - zip_path = "${path.module}/../../lambdas/dist/get-results-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "results" - http_method = "GET" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - - enable_cors = true - cors_allow_origin = "http://localhost:3000" + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "get-results" + zip_path = "${path.module}/../../lambdas/dist/get-results-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "results" + http_method = "GET" + + enable_cors = local.common_cors.enable_cors + cors_allow_origin = local.common_cors.cors_allow_origin cors_allow_methods = ["GET", "OPTIONS"] cors_allow_headers = ["Content-Type", "Authorization", "X-Requested-With", "X-Correlation-ID"] - environment_variables = { - NODE_OPTIONS = "--enable-source-maps" - DB_USERNAME = "app_user" - DB_ADDRESS = "postgres-db" - DB_PORT = "5432" - DB_NAME = "local_hometest_db" - DB_SCHEMA = "hometest" - DB_SECRET_NAME = "postgres-db-password" - DB_SSL = "false" - ALLOW_ORIGIN = "http://localhost:3000" - } + environment_variables = merge(local.common_base_env, local.common_db_env) } module "order_status_lambda" { source = "./modules/lambda" - project_name = var.project_name - function_name = "order-status" - zip_path = "${path.module}/../../lambdas/dist/order-status-lambda.zip" - lambda_role_arn = aws_iam_role.lambda_role.arn - environment = var.environment - api_gateway_id = aws_api_gateway_rest_api.api.id - api_gateway_root_resource_id = aws_api_gateway_rest_api.api.root_resource_id - api_gateway_execution_arn = aws_api_gateway_rest_api.api.execution_arn - api_path = "test-order/status" - http_method = "POST" - lambda_role_policy_attachment = aws_iam_role_policy_attachment.lambda_basic - - environment_variables = { - NODE_OPTIONS = "--enable-source-maps" - ALLOW_ORIGIN = "http://localhost:3000" - DB_USERNAME = "app_user" - DB_ADDRESS = "postgres-db" - DB_PORT = "5432" - DB_NAME = "local_hometest_db" - DB_SCHEMA = "hometest" - DB_SECRET_NAME = "postgres-db-password" - DB_SSL = "false" + project_name = local.common_lambda_params.project_name + aws_region = local.common_lambda_params.aws_region + function_name = "order-status" + zip_path = "${path.module}/../../lambdas/dist/order-status-lambda.zip" + lambda_role_arn = local.common_lambda_params.lambda_role_arn + environment = local.common_lambda_params.environment + api_gateway_id = local.common_lambda_params.api_gateway_id + api_gateway_root_resource_id = local.common_lambda_params.api_gateway_root_resource_id + api_gateway_execution_arn = local.common_lambda_params.api_gateway_execution_arn + api_path = "test-order/status" + http_method = "POST" + + environment_variables = merge(local.common_base_env, local.common_db_env, { NOTIFY_MESSAGES_QUEUE_URL = aws_sqs_queue.notify_messages.url HOME_TEST_BASE_URL = "http://localhost:3000" - } + }) } # API Gateway deployment diff --git a/local-environment/infra/modules/lambda/main.tf b/local-environment/infra/modules/lambda/main.tf index e188e997b..7e37e356c 100644 --- a/local-environment/infra/modules/lambda/main.tf +++ b/local-environment/infra/modules/lambda/main.tf @@ -10,8 +10,6 @@ resource "aws_lambda_function" "this" { environment { variables = var.environment_variables } - - depends_on = [var.lambda_role_policy_attachment] } locals { @@ -52,7 +50,7 @@ resource "aws_api_gateway_integration" "this" { integration_http_method = "POST" type = "AWS_PROXY" - uri = "arn:aws:apigateway:${data.aws_region.current.id}:lambda:path/2015-03-31/functions/${aws_lambda_function.this.arn}/invocations" + uri = "arn:aws:apigateway:${local.resolved_region}:lambda:path/2015-03-31/functions/${aws_lambda_function.this.arn}/invocations" } resource "aws_lambda_permission" "this" { @@ -63,9 +61,12 @@ resource "aws_lambda_permission" "this" { source_arn = "${var.api_gateway_execution_arn}/*/*" } -data "aws_region" "current" {} +data "aws_region" "current" { + count = var.aws_region == "" ? 1 : 0 +} locals { + resolved_region = var.aws_region != "" ? var.aws_region : data.aws_region.current[0].id cors_allow_methods = join(", ", var.cors_allow_methods) cors_allow_headers = join(", ", [for header in var.cors_allow_headers : lower(header)]) } diff --git a/local-environment/infra/modules/lambda/variables.tf b/local-environment/infra/modules/lambda/variables.tf index 7b3b1bbc1..9927db25c 100644 --- a/local-environment/infra/modules/lambda/variables.tf +++ b/local-environment/infra/modules/lambda/variables.tf @@ -3,6 +3,11 @@ variable "function_name" { type = string } variable "environment" { type = string } variable "zip_path" { type = string } variable "lambda_role_arn" { type = string } +variable "aws_region" { + type = string + default = "" + description = "AWS region. If empty, falls back to data.aws_region lookup." +} variable "handler" { type = string default = "index.handler" @@ -29,7 +34,6 @@ variable "authorization" { type = string default = "NONE" } -variable "lambda_role_policy_attachment" {} variable "enable_cors" { type = bool diff --git a/mise.lock b/mise.lock index 92ffea528..2054bb4be 100644 --- a/mise.lock +++ b/mise.lock @@ -1,37 +1,5 @@ # @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html -[[tools."aqua:aquasecurity/trivy"]] -version = "v0.69.3" -backend = "aqua:aquasecurity/trivy" - -[tools."aqua:aquasecurity/trivy"."platforms.linux-arm64"] -checksum = "sha256:7e3924a974e912e57b4a99f65ece7931f8079584dae12eb7845024f97087bdfd" -url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-ARM64.tar.gz" - -[tools."aqua:aquasecurity/trivy"."platforms.linux-arm64-musl"] -checksum = "sha256:7e3924a974e912e57b4a99f65ece7931f8079584dae12eb7845024f97087bdfd" -url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-ARM64.tar.gz" - -[tools."aqua:aquasecurity/trivy"."platforms.linux-x64"] -checksum = "sha256:1816b632dfe529869c740c0913e36bd1629cb7688bd5634f4a858c1d57c88b75" -url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.tar.gz" - -[tools."aqua:aquasecurity/trivy"."platforms.linux-x64-musl"] -checksum = "sha256:1816b632dfe529869c740c0913e36bd1629cb7688bd5634f4a858c1d57c88b75" -url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.tar.gz" - -[tools."aqua:aquasecurity/trivy"."platforms.macos-arm64"] -checksum = "sha256:a2f2179afd4f8bb265ca3c7aefb56a666bc4a9a411663bc0f22c3549fbc643a5" -url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_macOS-ARM64.tar.gz" - -[tools."aqua:aquasecurity/trivy"."platforms.macos-x64"] -checksum = "sha256:fec4a9f7569b624dd9d044fca019e5da69e032700edbb1d7318972c448ec2f4e" -url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_macOS-64bit.tar.gz" - -[tools."aqua:aquasecurity/trivy"."platforms.windows-x64"] -checksum = "sha256:74362dc711383255308230ecbeb587eb1e4e83a8d332be5b0259afac6e0c2224" -url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_windows-64bit.zip" - [[tools.awscli]] version = "2.34.26" backend = "aqua:aws/aws-cli" @@ -87,6 +55,38 @@ url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8 checksum = "sha256:d29144deff3a68aa93ced33dddf84b7fdc26070add4aa0f4513094c8332afc4e" url = "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_windows_x64.zip" +[[tools.grype]] +version = "0.111.0" +backend = "aqua:anchore/grype" + +[tools.grype."platforms.linux-arm64"] +checksum = "sha256:1a8b9bd691ce274e44056e7572cdf8c6970bdf9ec694001f7b4b17962b121b43" +url = "https://github.com/anchore/grype/releases/download/v0.111.0/grype_0.111.0_linux_arm64.tar.gz" + +[tools.grype."platforms.linux-arm64-musl"] +checksum = "sha256:1a8b9bd691ce274e44056e7572cdf8c6970bdf9ec694001f7b4b17962b121b43" +url = "https://github.com/anchore/grype/releases/download/v0.111.0/grype_0.111.0_linux_arm64.tar.gz" + +[tools.grype."platforms.linux-x64"] +checksum = "sha256:18ed2048d7a233566b681121d4632364f5f25d72cca86acc4c7ac57210d78a87" +url = "https://github.com/anchore/grype/releases/download/v0.111.0/grype_0.111.0_linux_amd64.tar.gz" + +[tools.grype."platforms.linux-x64-musl"] +checksum = "sha256:18ed2048d7a233566b681121d4632364f5f25d72cca86acc4c7ac57210d78a87" +url = "https://github.com/anchore/grype/releases/download/v0.111.0/grype_0.111.0_linux_amd64.tar.gz" + +[tools.grype."platforms.macos-arm64"] +checksum = "sha256:62d005a1e36ac7ec0b7be801ebc8eab0053fd831a227e1dc8ea9c356d38fa361" +url = "https://github.com/anchore/grype/releases/download/v0.111.0/grype_0.111.0_darwin_arm64.tar.gz" + +[tools.grype."platforms.macos-x64"] +checksum = "sha256:8fefd00f6ddd6407275be31b228089820e91c7a8cd2d046e877601773ac5062f" +url = "https://github.com/anchore/grype/releases/download/v0.111.0/grype_0.111.0_darwin_amd64.tar.gz" + +[tools.grype."platforms.windows-x64"] +checksum = "sha256:17f3bfb758b3c18426a89060344d9569f4344b0a606d42b60bd89792f996e3bd" +url = "https://github.com/anchore/grype/releases/download/v0.111.0/grype_0.111.0_windows_amd64.zip" + [[tools.node]] version = "24.14.1" backend = "core:node" @@ -119,6 +119,59 @@ url = "https://nodejs.org/dist/v24.14.1/node-v24.14.1-darwin-x64.tar.gz" checksum = "sha256:6e50ce5498c0cebc20fd39ab3ff5df836ed2f8a31aa093cecad8497cff126d70" url = "https://nodejs.org/dist/v24.14.1/node-v24.14.1-win-x64.zip" +[[tools.osv-scanner]] +version = "2.3.5" +backend = "aqua:google/osv-scanner" + +[tools.osv-scanner."platforms.linux-arm64"] +checksum = "sha256:fa46ad2b3954db5d5335303d45de921613393285d9a93c140b63b40e35e9ce50" +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/osv-scanner_linux_arm64" + +[tools.osv-scanner."platforms.linux-arm64".provenance.slsa] +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/multiple.intoto.jsonl" + +[tools.osv-scanner."platforms.linux-arm64-musl"] +checksum = "sha256:fa46ad2b3954db5d5335303d45de921613393285d9a93c140b63b40e35e9ce50" +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/osv-scanner_linux_arm64" + +[tools.osv-scanner."platforms.linux-arm64-musl".provenance.slsa] +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/multiple.intoto.jsonl" + +[tools.osv-scanner."platforms.linux-x64"] +checksum = "sha256:bb30c580afe5e757d3e959f4afd08a4795ea505ef84c46962b9a738aa573b41b" +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/osv-scanner_linux_amd64" + +[tools.osv-scanner."platforms.linux-x64".provenance.slsa] +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/multiple.intoto.jsonl" + +[tools.osv-scanner."platforms.linux-x64-musl"] +checksum = "sha256:bb30c580afe5e757d3e959f4afd08a4795ea505ef84c46962b9a738aa573b41b" +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/osv-scanner_linux_amd64" + +[tools.osv-scanner."platforms.linux-x64-musl".provenance.slsa] +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/multiple.intoto.jsonl" + +[tools.osv-scanner."platforms.macos-arm64"] +checksum = "sha256:b740efe0b08fb817865e818a498997d5f042f14b8eeafb6393176ce84dd09cf6" +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/osv-scanner_darwin_arm64" + +[tools.osv-scanner."platforms.macos-arm64".provenance.slsa] +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/multiple.intoto.jsonl" + +[tools.osv-scanner."platforms.macos-x64"] +checksum = "sha256:3b1c72d59dcbad99fa4eb2c72bf2e82017f83e0268340e4b00af76a1fea32c85" +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/osv-scanner_darwin_amd64" + +[tools.osv-scanner."platforms.macos-x64".provenance.slsa] +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/multiple.intoto.jsonl" + +[tools.osv-scanner."platforms.windows-x64"] +checksum = "sha256:b165d33c08bda663119a459f5187e096d2525f888503495f5f34925741e981a2" +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/osv-scanner_windows_amd64.exe" + +[tools.osv-scanner."platforms.windows-x64".provenance.slsa] +url = "https://github.com/google/osv-scanner/releases/download/v2.3.5/multiple.intoto.jsonl" + [[tools.pre-commit]] version = "4.5.1" backend = "aqua:pre-commit/pre-commit" diff --git a/package.json b/package.json index b9362d8cb..fa9cf181c 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,10 @@ "local:service:localstack:start": "npm run local:compose:up -- localstack", "local:service:localstack:stop": "npm run local:compose:down -- localstack", "local:terraform": "terraform -chdir=local-environment/infra", - "local:terraform:init": "npm run local:terraform -- init", + "local:terraform:init": "npm run local:terraform -- init -input=false", "local:terraform:plan": "npm run local:terraform -- plan", - "local:terraform:apply": "bash local-environment/scripts/localstack/ensure-localstack-running.sh && npm run local:terraform -- apply -auto-approve && npm run local:terraform:env", - "local:terraform:destroy": "npm run local:terraform -- destroy -auto-approve", + "local:terraform:apply": "bash local-environment/scripts/localstack/ensure-localstack-running.sh && npm run local:terraform -- apply -auto-approve -parallelism=30 && npm run local:terraform:env", + "local:terraform:destroy": "npm run local:terraform -- destroy -auto-approve -parallelism=30", "local:terraform:env": "bash scripts/terraform/post-apply-env-update.sh", "local:compose": "docker compose -f local-environment/docker-compose.yml", "local:compose:up": "npm run local:compose -- up -d", diff --git a/scripts/config/grype-table.tmpl b/scripts/config/grype-table.tmpl new file mode 100644 index 000000000..eb89090f9 --- /dev/null +++ b/scripts/config/grype-table.tmpl @@ -0,0 +1,4 @@ +NAME{{"\t"}}INSTALLED{{"\t"}}FIXED IN{{"\t"}}TYPE{{"\t"}}VULNERABILITY{{"\t"}}SEVERITY{{"\t"}}LOCATION +{{- range .Matches}} +{{.Artifact.Name}}{{"\t"}}{{.Artifact.Version}}{{"\t"}}{{if .Vulnerability.Fix.Versions}}{{index .Vulnerability.Fix.Versions 0}}{{end}}{{"\t"}}{{.Artifact.Type}}{{"\t"}}{{.Vulnerability.ID}}{{"\t"}}{{.Vulnerability.Severity}}{{"\t"}}{{if .Artifact.Locations}}{{(index .Artifact.Locations 0).Path}}{{end}} +{{- end}} diff --git a/scripts/config/grype.yaml b/scripts/config/grype.yaml index dfff9d0b3..d9d2dbf61 100644 --- a/scripts/config/grype.yaml +++ b/scripts/config/grype.yaml @@ -2,6 +2,13 @@ # If using SBOM input, automatically generate CPEs when packages have none add-cpes-if-none: true +# Fail with exit code 2 when vulnerabilities >= this severity are found +# options: negligible, low, medium, high, critical +fail-on-severity: high + +exclude: + - ./**/.terraform/** + # ignore: # # This is the full set of supported rule fields: # - vulnerability: CVE-2008-4318 diff --git a/scripts/grype-scan.sh b/scripts/grype-scan.sh new file mode 100755 index 000000000..c12285172 --- /dev/null +++ b/scripts/grype-scan.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +grype "dir:${PROJECT_DIR}" \ + -o template \ + -t "${PROJECT_DIR}/scripts/config/grype-table.tmpl" \ + --name hometest-service \ + -c "${PROJECT_DIR}/scripts/config/grype.yaml"