diff --git a/.github/workflows/security-trivy-scan-callable.yml b/.github/workflows/security-trivy-scan-callable.yml index 69c438a07c..9ecc3b0174 100644 --- a/.github/workflows/security-trivy-scan-callable.yml +++ b/.github/workflows/security-trivy-scan-callable.yml @@ -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<> $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' + # 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)) + + # Get unique CVE count + UNIQUE_CVES=$(jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | length' trivy-results.json) + + # Get affected packages count + AFFECTED_PACKAGES=$(jq -r '[.Results[]?.Vulnerabilities[]? | .PkgName] | unique | length' trivy-results.json) + + 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) ** (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)) | + .[: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: - 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* - - *Repository:* `${{ github.repository }}` - *Image Ref:* `${{ inputs.image_ref }}` - - No vulnerabilities detected in the Docker image scan. - - :point_right: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Scan Results> + 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 }} diff --git a/.gitignore b/.gitignore index b4b25b34f4..d912149efb 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ compiled_app_output trivy_report* compiled packages/cli/src/modules/my-feature +.secrets