Skip to content
Draft
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
143 changes: 143 additions & 0 deletions .github/actions/trivy-fs-scan/action.yaml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
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<details><summary>Findings (top 50)</summary>\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</details>\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
129 changes: 124 additions & 5 deletions .github/workflows/preview-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,24 +122,143 @@ 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
run: |
echo "function = ${{ steps.names.outputs.function_name }}"
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@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
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
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
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"
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"
if [ -f /tmp/preview.headers ]; then
echo "Response headers:"
cat /tmp/preview.headers
fi
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: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Preview Lambda: \`${fn}\`\nPreview URL: ${url}`
owner,
repo,
issue_number: issueNumber,
body: lines.join('\n'),
});

# ---------- Perform trivy scan and notify ----------
- 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 }}
Loading