From d4a83f6ac6399747dbec99c8c165d84aadf4349a Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 19 Jan 2026 16:48:19 +0000 Subject: [PATCH 01/11] Make it different --- .github/workflows/preview-env.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index ebd803b..97f44b2 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -122,7 +122,7 @@ jobs: if: github.event.action == 'closed' run: | FN="${{ steps.names.outputs.function_name }}" - echo "Deleting preview function: $FN" + echo "Deleting preview function: $FN" aws lambda delete-function --function-name "$FN" || true - name: Output function name From f3ce66972304ae9bfd3a1e862849de90d12bb139 Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 11:17:01 +0000 Subject: [PATCH 02/11] [CDAPI-68] Add trivy fs scan --- .github/actions/trivy-fs-scan/action.yaml | 143 ++++++++++++++++++++++ .github/workflows/preview-env.yaml | 15 +++ 2 files changed, 158 insertions(+) create mode 100644 .github/actions/trivy-fs-scan/action.yaml diff --git a/.github/actions/trivy-fs-scan/action.yaml b/.github/actions/trivy-fs-scan/action.yaml new file mode 100644 index 0000000..c033ade --- /dev/null +++ b/.github/actions/trivy-fs-scan/action.yaml @@ -0,0 +1,143 @@ +name: Trivy fs Scan +description: Performs Trivy security scanning for filesystems with comprehensive reporting + +inputs: + filesystem-ref: + description: 'Filesystem reference to scan (e.g., /path/to/filesystem)' + required: true + severity: + description: 'Comma-separated list of severity levels to report' + required: false + default: 'HIGH,CRITICAL,MEDIUM,LOW,UNKNOWN' + trivy-config: + description: 'Path to Trivy configuration file' + required: false + default: 'trivy.yaml' + artifact-name: + description: 'Name for the uploaded artifact' + required: false + default: 'trivy-fs-scan-results' + fail-on-critical-high: + description: 'Whether to fail the action on critical/high findings' + required: false + default: 'true' + ignore-unfixed: + description: 'Ignore unfixed vulnerabilities' + required: false + default: 'true' + +outputs: + critical-count: + description: 'Number of critical severity findings' + value: ${{ steps.report.outputs.crit }} + high-count: + description: 'Number of high severity findings' + value: ${{ steps.report.outputs.high }} + report-path: + description: 'Path to the generated markdown report' + value: 'trivy_fs_report.md' + +runs: + using: "composite" + steps: + - name: Check if Trivy config exists + id: trivy-config-check + shell: bash + run: | + if [[ -f "${{ inputs.trivy-config }}" ]]; then + echo "config-exists=true" >> "$GITHUB_OUTPUT" + echo "config-arg=${{ inputs.trivy-config }}" >> "$GITHUB_OUTPUT" + else + echo "config-exists=false" >> "$GITHUB_OUTPUT" + echo "config-arg=" >> "$GITHUB_OUTPUT" + fi + + - name: Trivy fs scan + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: 'fs' + scan-ref: ${{ inputs.filesystem-ref }} + format: json + output: trivy-fs-scan.json + exit-code: 0 + ignore-unfixed: ${{ inputs.ignore-unfixed }} + severity: ${{ inputs.severity }} + trivy-config: ${{ steps.trivy-config-check.outputs.config-arg }} + + - name: Upload Trivy Scan Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.artifact-name }} + path: trivy-fs-scan.json + + - name: Build summary & counts + id: report + shell: bash + run: | + # Filesystem scan report + jq -r ' + def clean: (.|tostring) | gsub("\\|";"\\|") | gsub("\r?\n";" "); + def sev: ["CRITICAL","HIGH","MEDIUM","LOW","UNKNOWN"]; + def counts: + ([.Results[]? | .Vulnerabilities[]? | .Severity // "UNKNOWN"] + | reduce .[] as $s ({CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0,UNKNOWN:0}; .[$s]+=1)); + . as $root + | (counts) as $c + | + # ---- TOP BANNER (only if High/Critical present) ---- + ( + if (($c.CRITICAL + $c.HIGH) > 0) then + "🚫 **Trivy gate:** **\($c.CRITICAL) Critical**, **\($c.HIGH) High** vulnerability(s) found.\n\n" + else + "āœ… **Trivy gate:** no Critical/High vulnerabilities.\n\n" + end + ) + # ---- SUMMARY REPORT ---- + + "### Trivy Filesystem Scan Summary\n\n" + + "**Filesystem:** " + ($root.ArtifactName // "'"'"'${{ inputs.filesystem-ref }}'"'"'") + "\n\n" + + "| Severity | Count |\n|---|---|\n" + + (sev | map("| " + . + " | " + ($c[.]|tostring) + " |") | join("\n")) + + (if ([.Results[]? | .Vulnerabilities[]?] | length) == 0 + then "\n\nāœ… No vulnerabilities found.\n" + else + "\n\n
Findings (top 50)\n\n" + + "| Severity | ID | Package | Installed | Fixed | Source |\n|---|---|---|---|---|---|\n" + + ( + [ .Results[]? as $r + | $r.Vulnerabilities[]? + | "| \(.Severity) | \(.VulnerabilityID) | \(.PkgName) | \(.InstalledVersion) | \((.FixedVersion // "") | clean) | \(($r.Target) | clean) |" + ] | .[:50] | join("\n") + ) + + "\n\n
\n" + end) + ' trivy-fs-scan.json > trivy_fs_report.md + + # Extract counts for gating/other steps + read CRIT HIGH < <(jq -r ' + [.Results[]? | .Vulnerabilities[]? | .Severity // "UNKNOWN"] + | reduce .[] as $s ({CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0,UNKNOWN:0}; .[$s]+=1) + | "\(.CRITICAL) \(.HIGH)" + ' trivy-fs-scan.json) + + echo "crit=$CRIT" >> "$GITHUB_OUTPUT" + echo "high=$HIGH" >> "$GITHUB_OUTPUT" + + - name: Publish Trivy Summary + if: always() + shell: bash + run: cat trivy_fs_report.md >> "$GITHUB_STEP_SUMMARY" + + - name: Update Trivy PR comment + if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: ${{ inputs.artifact-name }} + path: trivy_fs_report.md + + - name: Check Trivy Issue Thresholds + if: ${{ inputs.fail-on-critical-high == 'true' && (steps.report.outputs.crit != '0' || steps.report.outputs.high != '0') }} + shell: bash + run: | + echo "Critical vulnerabilities detected: ${{ steps.report.outputs.crit }}" + echo "High vulnerabilities detected: ${{ steps.report.outputs.high }}" + exit 1 \ No newline at end of file diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 97f44b2..6c9bf95 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -143,3 +143,18 @@ jobs: issue_number: context.issue.number, body: `Preview Lambda: \`${fn}\`\nPreview URL: ${url}` }); + + - name: Prepare lambda artifact for trivy scan + if: github.event.action != 'closed' + run: | + cd infrastructure/environments/preview + rm -rf /tmp/artifact + mkdir -p /tmp/artifact + unzip -q artifact.zip -d /tmp/artifact + + - name: Trivy filesystem scan + if: github.event.action != 'closed' + uses: ./.github/actions/trivy-fs-scan + with: + filesystem-ref: /tmp/artifact + artifact-name: trivy-fs-scan-${{ steps.branch.outputs.safe }} From 17794b01cd79769508bf8e417cfa52a8e26e79f3 Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 11:21:38 +0000 Subject: [PATCH 03/11] Feeding the file format grue's --- .github/actions/trivy-fs-scan/action.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/trivy-fs-scan/action.yaml b/.github/actions/trivy-fs-scan/action.yaml index c033ade..c8bdf2c 100644 --- a/.github/actions/trivy-fs-scan/action.yaml +++ b/.github/actions/trivy-fs-scan/action.yaml @@ -83,7 +83,7 @@ runs: | reduce .[] as $s ({CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0,UNKNOWN:0}; .[$s]+=1)); . as $root | (counts) as $c - | + | # ---- TOP BANNER (only if High/Critical present) ---- ( if (($c.CRITICAL + $c.HIGH) > 0) then @@ -111,14 +111,14 @@ runs: + "\n\n\n" end) ' trivy-fs-scan.json > trivy_fs_report.md - + # Extract counts for gating/other steps read CRIT HIGH < <(jq -r ' [.Results[]? | .Vulnerabilities[]? | .Severity // "UNKNOWN"] | reduce .[] as $s ({CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0,UNKNOWN:0}; .[$s]+=1) | "\(.CRITICAL) \(.HIGH)" ' trivy-fs-scan.json) - + echo "crit=$CRIT" >> "$GITHUB_OUTPUT" echo "high=$HIGH" >> "$GITHUB_OUTPUT" @@ -140,4 +140,4 @@ runs: run: | echo "Critical vulnerabilities detected: ${{ steps.report.outputs.crit }}" echo "High vulnerabilities detected: ${{ steps.report.outputs.high }}" - exit 1 \ No newline at end of file + exit 1 From a39686b0f9edfe1950c0dd5f72dc6f4330591ea8 Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 11:34:26 +0000 Subject: [PATCH 04/11] Add smoke testing --- .github/workflows/preview-env.yaml | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 6c9bf95..57ea9f1 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -130,6 +130,99 @@ jobs: echo "function = ${{ steps.names.outputs.function_name }}" echo "url = ${{ steps.names.outputs.preview_url }}" + # ---------- Wait on AWS tasks and notify ---------- + - name: Smoke test preview URL + if: github.event.action != 'closed' + id: smoke-test + env: + PREVIEW_URL: ${{ steps.names.outputs.preview_url }} + run: | + if [ -z "$PREVIEW_URL" ] || [ "$PREVIEW_URL" = "null" ]; then + echo "Preview URL missing" + echo "http_status=missing" >> "$GITHUB_OUTPUT" + echo "http_result=missing-url" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Reachability check: allow 404 (app routes might not exist yet) but fail otherwise + STATUS=$(curl --silent --output /tmp/preview.headers --write-out '%{http_code}' --head --max-time 30 "$PREVIEW_URL" || true) + + if [ "$STATUS" = "404" ]; then + echo "Preview responded with expected 404" + echo "http_status=404" >> "$GITHUB_OUTPUT" + echo "http_result=allowed-404" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [[ "$STATUS" =~ ^[0-9]{3}$ ]] && [ "$STATUS" -ge 200 ] && [ "$STATUS" -lt 400 ]; then + echo "Preview responded with status $STATUS" + echo "http_status=$STATUS" >> "$GITHUB_OUTPUT" + echo "http_result=success" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "Preview responded with unexpected status $STATUS" + cat /tmp/preview.headers + echo "http_status=$STATUS" >> "$GITHUB_OUTPUT" + echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT" + exit 0 + + - name: Comment function name on PR + if: github.event_name == 'pull_request' && github.event.action != 'closed' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd + with: + script: | + const fn = '${{ steps.names.outputs.function_name }}'; + const url = '${{ steps.names.outputs.preview_url }}'; + const owner = context.repo.owner; + const repo = context.repo.repo; + const issueNumber = context.issue.number; + const smokeStatus = '${{ steps.smoke-test.outputs.http_status }}' || 'n/a'; + const smokeResult = '${{ steps.smoke-test.outputs.http_result }}' || 'not-run'; + + const smokeLabels = { + success: ':white_check_mark: Passed', + 'allowed-404': ':white_check_mark: Allowed 404', + 'unexpected-status': ':x: Unexpected status', + 'missing-url': ':x: Missing URL', + }; + + const smokeReadable = smokeLabels[smokeResult] ?? smokeResult; + + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: issueNumber, + per_page: 100, + }); + + for (const comment of comments) { + const isBot = comment.user?.login === 'github-actions[bot]'; + const isPreviewUpdate = comment.body?.includes('Deployment Complete'); + + if (isBot && isPreviewUpdate) { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: comment.id, + }); + } + } + + const lines = [ + '**Deployment Complete**', + `- Preview URL: [${url}](${url})`, + `- Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`, + `- Lambda Function: ${fn}`, + ]; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issueNumber, + body: lines.join('\n'), + }); + - name: Comment function name on PR if: github.event_name == 'pull_request' && github.event.action != 'closed' uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd @@ -144,6 +237,7 @@ jobs: body: `Preview Lambda: \`${fn}\`\nPreview URL: ${url}` }); + # ---------- Perform trivy scan and notify ---------- - name: Prepare lambda artifact for trivy scan if: github.event.action != 'closed' run: | From e7c248b89b851cc1979a0cc81a4935771c4fe977 Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 11:58:00 +0000 Subject: [PATCH 05/11] Error handling --- .github/workflows/preview-env.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 57ea9f1..08e6d1f 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -162,6 +162,10 @@ jobs: fi echo "Preview responded with unexpected status $STATUS" + if [ -f /tmp/preview.headers ]; then + echo "Response headers:" + cat /tmp/preview.headers + fi cat /tmp/preview.headers echo "http_status=$STATUS" >> "$GITHUB_OUTPUT" echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT" From 60b1ec293b95c9b51675736de398c2fe3d6f0b12 Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 11:59:15 +0000 Subject: [PATCH 06/11] Doh - remove the failed op --- .github/workflows/preview-env.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 08e6d1f..dd7f5aa 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -166,7 +166,6 @@ jobs: echo "Response headers:" cat /tmp/preview.headers fi - cat /tmp/preview.headers echo "http_status=$STATUS" >> "$GITHUB_OUTPUT" echo "http_result=unexpected-status" >> "$GITHUB_OUTPUT" exit 0 From db7a5c79d687040b6f8714764d094e8fdac16b6a Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 12:01:32 +0000 Subject: [PATCH 07/11] Remove redundent commenting --- .github/workflows/preview-env.yaml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index dd7f5aa..5b3b2d3 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -226,20 +226,6 @@ jobs: body: lines.join('\n'), }); - - name: Comment function name on PR - if: github.event_name == 'pull_request' && github.event.action != 'closed' - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd - with: - script: | - const fn = '${{ steps.names.outputs.function_name }}'; - const url = '${{ steps.names.outputs.preview_url }}'; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `Preview Lambda: \`${fn}\`\nPreview URL: ${url}` - }); - # ---------- Perform trivy scan and notify ---------- - name: Prepare lambda artifact for trivy scan if: github.event.action != 'closed' From dbbb6cd1b8808b7eb2338ef99b7501af690a6dad Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 12:09:16 +0000 Subject: [PATCH 08/11] Try to retrieve secrets --- .github/workflows/preview-env.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 5b3b2d3..601199e 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -131,6 +131,16 @@ jobs: echo "url = ${{ steps.names.outputs.preview_url }}" # ---------- Wait on AWS tasks and notify ---------- + - name: Get mTLS certs for testing + if: github.event.action != 'closed' + id: mtls-certs + uses: aws-actions/aws-secretsmanager-get-secrets@v2 + with: + secret-ids: | + /cds/pathology/dev/mtls/client1-key-secret + /cds/pathology/dev/mtls/client1-key-public + name-transformation: lowercase + - name: Smoke test preview URL if: github.event.action != 'closed' id: smoke-test From e25661f1c814f91a59f3ddf348cc4eedb72a85fe Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 12:24:39 +0000 Subject: [PATCH 09/11] Use mTLSA client certs for smoke check --- .github/workflows/preview-env.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index 601199e..aaabb53 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -155,7 +155,18 @@ jobs: fi # Reachability check: allow 404 (app routes might not exist yet) but fail otherwise - STATUS=$(curl --silent --output /tmp/preview.headers --write-out '%{http_code}' --head --max-time 30 "$PREVIEW_URL" || true) + printf '%s' "$_cds_pathology_dev_mtls_client1_key_secret" > /tmp/client1-key.pem + printf '%s' "$_cds_pathology_dev_mtls_client1_key_public" > /tmp/client1-cert.pem + STATUS=$(curl \ + --cert /tmp/client1-cert.pem \ + --key /tmp/client1-key.pem \ + --silent \ + --output /tmp/preview.headers \ + --write-out '%{http_code}' \ + --head \ + --max-time 30 "$PREVIEW_URL" || true) + rm -f /tmp/client1-key.pem + rm -f /tmp/client1-cert.pem if [ "$STATUS" = "404" ]; then echo "Preview responded with expected 404" From 1c92506edf307425a7b0260937aec060843e1347 Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 12:26:17 +0000 Subject: [PATCH 10/11] Feeding the linting grue. --- .github/workflows/preview-env.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index aaabb53..e493563 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -158,13 +158,13 @@ jobs: printf '%s' "$_cds_pathology_dev_mtls_client1_key_secret" > /tmp/client1-key.pem printf '%s' "$_cds_pathology_dev_mtls_client1_key_public" > /tmp/client1-cert.pem STATUS=$(curl \ - --cert /tmp/client1-cert.pem \ - --key /tmp/client1-key.pem \ - --silent \ - --output /tmp/preview.headers \ - --write-out '%{http_code}' \ - --head \ - --max-time 30 "$PREVIEW_URL" || true) + --cert /tmp/client1-cert.pem \ + --key /tmp/client1-key.pem \ + --silent \ + --output /tmp/preview.headers \ + --write-out '%{http_code}' \ + --head \ + --max-time 30 "$PREVIEW_URL" || true) rm -f /tmp/client1-key.pem rm -f /tmp/client1-cert.pem From 2a8994fa6f58721c9d560c48e491045c5e5cad41 Mon Sep 17 00:00:00 2001 From: neil-sproston Date: Mon, 26 Jan 2026 12:29:08 +0000 Subject: [PATCH 11/11] Pin aws-action --- .github/workflows/preview-env.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-env.yaml b/.github/workflows/preview-env.yaml index e493563..6f945ef 100644 --- a/.github/workflows/preview-env.yaml +++ b/.github/workflows/preview-env.yaml @@ -134,7 +134,7 @@ jobs: - name: Get mTLS certs for testing if: github.event.action != 'closed' id: mtls-certs - uses: aws-actions/aws-secretsmanager-get-secrets@v2 + uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802 with: secret-ids: | /cds/pathology/dev/mtls/client1-key-secret