Skip to content
Merged
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
265 changes: 174 additions & 91 deletions .github/workflows/security-trivy-scan-callable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,123 +4,206 @@ on:
workflow_dispatch:
inputs:
image_ref:
description: Full image reference to scan e.g ghcr.io/n8n-io/n8n:latest
description: 'Full image reference to scan e.g. ghcr.io/n8n-io/n8n:latest'
required: true
default: 'ghcr.io/n8n-io/n8n:latest'
workflow_call:
inputs:
image_ref:
type: string
description: Full image reference to scan e.g ghcr.io/n8n-io/n8n:latest
description: 'Full image reference to scan e.g. ghcr.io/n8n-io/n8n:latest'
required: true
secrets:
SLACK_WEBHOOK_URL:
SLACK_BOT_TOKEN:
required: true

permissions:
contents: read

env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: C042WDXPTEZ #mission-security

jobs:
security_scan:
name: Security - Scan Docker Image With Trivy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Pull Docker image with retry
run: |
for i in {1..4}; do
docker pull "${{ inputs.image_ref }}" && break
[ $i -lt 4 ] && echo "Retry $i failed, waiting..." && sleep 15
done

- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.30.0
uses: aquasecurity/trivy-action@dc5a429b52fcf669ce959baa2c2dd26090d2a6c4 # v0.32.0
id: trivy_scan
with:
image-ref: ${{ inputs.image_ref }}
format: 'json'
output: 'trivy-results.json'
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'
ignore-unfixed: false
exit-code: '0'

- name: Process vulnerability results
id: process_vulns
- name: Calculate vulnerability counts
id: process_results
run: |
if [ -f trivy-results.json ]; then
CRITICAL_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' trivy-results.json)
HIGH_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' trivy-results.json)
MEDIUM_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "MEDIUM")] | length' trivy-results.json)
LOW_COUNT=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "LOW")] | length' trivy-results.json)

TOTAL_VULNS=$((CRITICAL_COUNT + HIGH_COUNT + MEDIUM_COUNT + LOW_COUNT))

echo "critical_count=${CRITICAL_COUNT}" >> $GITHUB_OUTPUT
echo "high_count=${HIGH_COUNT}" >> $GITHUB_OUTPUT
echo "medium_count=${MEDIUM_COUNT}" >> $GITHUB_OUTPUT
echo "low_count=${LOW_COUNT}" >> $GITHUB_OUTPUT
echo "total_count=${TOTAL_VULNS}" >> $GITHUB_OUTPUT

if [ $TOTAL_VULNS -gt 0 ]; then
echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT

# Extract top vulnerabilities for display (limit to 10 for readability)
TOP_VULNS=$(jq -r '
.Results[]?
| .Vulnerabilities[]?
| select(.Severity == "CRITICAL" or .Severity == "HIGH" or .Severity == "MEDIUM" or .Severity == "LOW")
| "• \(.VulnerabilityID): \(.Title // "No title") (\(.Severity))"
' trivy-results.json | head -10)

echo "top_vulnerabilities<<EOF" >> $GITHUB_OUTPUT
echo "$TOP_VULNS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT
fi
else
echo "Trivy results file not found."
if [ ! -s trivy-results.json ] || [ $(jq '.Results | length' trivy-results.json) -eq 0 ]; then
echo "No high-severity vulnerabilities found."
echo "vulnerabilities_found=false" >> $GITHUB_OUTPUT
exit 0
fi

- name: Notify Slack - Vulnerabilities Found
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: steps.process_vulns.outputs.vulnerabilities_found == 'true'
with:
status: 'warning'
channel: '#mission-security'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: |
:warning: *Trivy Scan: Vulnerabilities Detected*

*Repository:* `${{ github.repository }}`
*Image Ref:* `${{ inputs.image_ref }}`

*Vulnerability Summary:*
• *Critical:* ${{ steps.process_vulns.outputs.critical_count }}
• *High:* ${{ steps.process_vulns.outputs.high_count }}
• *Medium:* ${{ steps.process_vulns.outputs.medium_count }}
• *Low:* ${{ steps.process_vulns.outputs.low_count }}
• *Total:* ${{ steps.process_vulns.outputs.total_count }}

*Top Vulnerabilities (showing first 10):*
```
${{ steps.process_vulns.outputs.top_vulnerabilities }}
```

:point_right: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Full Scan Results>

- name: Notify Slack - No Vulnerabilities
uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0
if: steps.process_vulns.outputs.vulnerabilities_found == 'false'
with:
status: 'success'
channel: '#mission-security'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: |
:white_check_mark: *Trivy Scan: All Clear*
# Calculate counts by severity
CRITICAL_COUNT=$(jq '([.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length)' trivy-results.json)
HIGH_COUNT=$(jq '([.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length)' trivy-results.json)
TOTAL_VULNS=$((CRITICAL_COUNT + HIGH_COUNT))

*Repository:* `${{ github.repository }}`
*Image Ref:* `${{ inputs.image_ref }}`
# Get unique CVE count
UNIQUE_CVES=$(jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | length' trivy-results.json)

No vulnerabilities detected in the Docker image scan.
# Get affected packages count
AFFECTED_PACKAGES=$(jq -r '[.Results[]?.Vulnerabilities[]? | .PkgName] | unique | length' trivy-results.json)

:point_right: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Scan Results>
echo "vulnerabilities_found=$( [ $TOTAL_VULNS -gt 0 ] && echo 'true' || echo 'false' )" >> $GITHUB_OUTPUT
echo "total_count=$TOTAL_VULNS" >> $GITHUB_OUTPUT
echo "critical_count=$CRITICAL_COUNT" >> $GITHUB_OUTPUT
echo "high_count=$HIGH_COUNT" >> $GITHUB_OUTPUT
echo "unique_cves=$UNIQUE_CVES" >> $GITHUB_OUTPUT
echo "affected_packages=$AFFECTED_PACKAGES" >> $GITHUB_OUTPUT

- name: Generate GitHub Job Summary
if: always()
run: |
{
echo "# 🛡️ Trivy Security Scan Results"
echo ""
echo "**Image:** \`${{ inputs.image_ref }}\`"
echo "**Scan Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo ""
} >> $GITHUB_STEP_SUMMARY

if [ "${{ steps.process_results.outputs.vulnerabilities_found }}" == "false" ]; then
echo "✅ **No critical or high severity vulnerabilities found!**" >> $GITHUB_STEP_SUMMARY
else
{
echo "## 📊 Summary"
echo "| Metric | Count |"
echo "|--------|-------|"
echo "| 🔴 Critical Vulnerabilities | ${{ steps.process_results.outputs.critical_count }} |"
echo "| 🟠 High Vulnerabilities | ${{ steps.process_results.outputs.high_count }} |"
echo "| 📋 Unique CVEs | ${{ steps.process_results.outputs.unique_cves }} |"
echo "| 📦 Affected Packages | ${{ steps.process_results.outputs.affected_packages }} |"
echo ""
echo "## 🚨 Top Vulnerabilities"
echo ""
} >> $GITHUB_STEP_SUMMARY

# Generate detailed vulnerability table
jq -r --arg image_ref "${{ inputs.image_ref }}" '
# Collect all vulnerabilities
[.Results[] | select(.Vulnerabilities != null) | .Vulnerabilities[]] |
# Group by CVE ID to avoid duplicates
group_by(.VulnerabilityID) |
map({
cve: .[0].VulnerabilityID,
severity: .[0].Severity,
cvss: (.[0].CVSS.nvd.V3Score // "N/A"),
cvss_sort: (.[0].CVSS.nvd.V3Score // 0),
packages: [.[] | "\(.PkgName)@\(.InstalledVersion)"] | unique | join(", "),
fixed: (.[0].FixedVersion // "No fix available"),
description: (.[0].Description // "No description available") | split("\n")[0] | .[0:150]
}) |
# Sort by severity (CRITICAL first) and CVSS score
sort_by((.severity == "HIGH" | if . then 1 else 0 end), -.cvss_sort) |
# Take top 15
.[:15] |
# Generate markdown table
"| CVE | Severity | CVSS | Package(s) | Fix Version | Description |",
"|-----|----------|------|------------|-------------|-------------|",
(.[] | "| [\(.cve)](https://nvd.nist.gov/vuln/detail/\(.cve)) | \(.severity) | \(.cvss) | `\(.packages)` | `\(.fixed)` | \(.description) |")
' trivy-results.json >> $GITHUB_STEP_SUMMARY

{
echo ""
echo "---"
echo "🔍 **View detailed logs above for full analysis**"
} >> $GITHUB_STEP_SUMMARY
fi

- name: Generate Slack Blocks JSON
if: steps.process_results.outputs.vulnerabilities_found == 'true'
id: generate_blocks
run: |
BLOCKS_JSON=$(jq -c --arg image_ref "${{ inputs.image_ref }}" \
--arg repo_url "${{ github.server_url }}/${{ github.repository }}" \
--arg repo_name "${{ github.repository }}" \
--arg run_url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--arg critical_count "${{ steps.process_results.outputs.critical_count }}" \
--arg high_count "${{ steps.process_results.outputs.high_count }}" \
--arg unique_cves "${{ steps.process_results.outputs.unique_cves }}" \
'
# Function to create a vulnerability block with emoji indicators
def vuln_block: {
"type": "section",
"text": {
"type": "mrkdwn",
"text": "\(if .Severity == "CRITICAL" then ":red_circle:" else ":large_orange_circle:" end) *<https://nvd.nist.gov/vuln/detail/\(.VulnerabilityID)|\(.VulnerabilityID)>* (CVSS: `\(.CVSS.nvd.V3Score // "N/A")`)\n*Package:* `\(.PkgName)@\(.InstalledVersion)` → `\(.FixedVersion // "No fix available")`"
}
};

# Main structure
[
{
"type": "header",
"text": { "type": "plain_text", "text": ":warning: Trivy Scan: Vulnerabilities Detected" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Repository:*\n<\($repo_url)|\($repo_name)>" },
{ "type": "mrkdwn", "text": "*Image:*\n`\($image_ref)`" },
{ "type": "mrkdwn", "text": "*Critical:*\n:red_circle: \($critical_count)" },
{ "type": "mrkdwn", "text": "*High:*\n:large_orange_circle: \($high_count)" }
]
},
{
"type": "context",
"elements": [
{ "type": "mrkdwn", "text": ":shield: \($unique_cves) unique CVEs affecting packages" }
]
},
{ "type": "divider" }
] +
(
# Group vulnerabilities by CVE to avoid duplicates in notification
[.Results[] | select(.Vulnerabilities != null) | .Vulnerabilities[]] |
group_by(.VulnerabilityID) |
map(.[0]) |
sort_by((.Severity == "HIGH" | if . then 1 else 0 end), -((.CVSS.nvd.V3Score // 0) | tonumber? // 0)) |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid two-parameter call to sort_by will make the Slack-block jq script crash, so no Slack message will be posted.

Prompt for AI agents
Address the following comment on .github/workflows/security-trivy-scan-callable.yml at line 178:

<comment>Invalid two-parameter call to sort_by will make the Slack-block jq script crash, so no Slack message will be posted.</comment>

<file context>
@@ -4,123 +4,206 @@ on:
   workflow_dispatch:
     inputs:
       image_ref:
-        description: Full image reference to scan e.g ghcr.io/n8n-io/n8n:latest
+        description: &#39;Full image reference to scan e.g. ghcr.io/n8n-io/n8n:latest&#39;
         required: true
         default: &#39;ghcr.io/n8n-io/n8n:latest&#39;
   workflow_call:
     inputs:
</file context>
Suggested change
sort_by((.Severity == "HIGH" | if . then 1 else 0 end), -((.CVSS.nvd.V3Score // 0) | tonumber? // 0)) |
sort_by([(.Severity == "HIGH" | if . then 1 else 0 end), -((.CVSS.nvd.V3Score // 0) | tonumber? // 0)]) |

.[:8] |
map(. | vuln_block)
) +
[
{ "type": "divider" },
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": ":github: View Full Report" },
"style": "primary",
"url": $run_url
}
]
}
]
' trivy-results.json)

echo "slack_blocks=$BLOCKS_JSON" >> $GITHUB_OUTPUT

- name: Send Slack Notification
if: steps.process_results.outputs.vulnerabilities_found == 'true'
uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ env.SLACK_CHANNEL_ID }}
text: "🚨 Trivy Scan: ${{ steps.process_results.outputs.critical_count }} Critical, ${{ steps.process_results.outputs.high_count }} High vulnerabilities found in ${{ inputs.image_ref }}"
blocks: ${{ steps.generate_blocks.outputs.slack_blocks }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ compiled_app_output
trivy_report*
compiled
packages/cli/src/modules/my-feature
.secrets
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, is this for something local development or?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's for "ACT" which lets you run the workflows locally. It's a lifesaver for work like this.

Loading