name: Security - Scan Docker Image With Trivy

on:
  workflow_dispatch:
    inputs:
      image_ref:
        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'
        required: true
    secrets:
      QBOT_SLACK_TOKEN:
        required: true

permissions:
  contents: read

env:
  SLACK_CHANNEL_ID: C0AHNJU9XFA #updates-security

jobs:
  security_scan:
    name: Security - Scan Docker Image With Trivy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout for VEX file
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
        with:
          sparse-checkout: |
            security/vex.openvex.json
            security/trivy.yaml
            security/trivy-ignore-policy.rego
            .github/scripts/retry.mjs
            .github/scripts/slack
          sparse-checkout-cone-mode: false

      - name: Pull Docker image with retry
        env:
          IMAGE_REF: ${{ inputs.image_ref }}
        run: node .github/scripts/retry.mjs --attempts 4 --delay 15 -- docker pull "$IMAGE_REF"

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # v0.34.1
        id: trivy_scan
        with:
          image-ref: ${{ inputs.image_ref }}
          version: 'v0.69.2'
          format: 'json'
          output: 'trivy-results.json'
          severity: 'CRITICAL,HIGH,MEDIUM,LOW'
          ignore-unfixed: false
          exit-code: '0'
          trivy-config: 'security/trivy.yaml'

      - name: Calculate vulnerability counts
        id: process_results
        run: |
          if [ ! -s trivy-results.json ] || [ "$(jq '.Results | length' trivy-results.json)" -eq 0 ]; then
            echo "No vulnerabilities found."
            echo "vulnerabilities_found=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          # 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)
          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))

          # 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' )"
            echo "total_count=$TOTAL_VULNS"
            echo "critical_count=$CRITICAL_COUNT"
            echo "high_count=$HIGH_COUNT"
            echo "medium_count=$MEDIUM_COUNT"
            echo "low_count=$LOW_COUNT"
            echo "unique_cves=$UNIQUE_CVES"
            echo "affected_packages=$AFFECTED_PACKAGES"
          } >> "$GITHUB_OUTPUT"

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

          if [ ! -s trivy-results.json ]; then
            {
              echo "⚠️ **Scan did not produce results.** Check the 'Run Trivy vulnerability scanner' step for errors."
            } >> "$GITHUB_STEP_SUMMARY"
          elif [ "${{ steps.process_results.outputs.vulnerabilities_found }}" == "false" ]; then
            {
              echo "✅ **No 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 "| 🟡 Medium Vulnerabilities | ${{ steps.process_results.outputs.medium_count }} |"
              echo "| 🟡 Low Vulnerabilities | ${{ steps.process_results.outputs.low_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 "$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, HIGH, MEDIUM, LOW) and CVSS score
                sort_by(
                  if .severity == "CRITICAL" then 0
                  elif .severity == "HIGH" then 1
                  elif .severity == "MEDIUM" then 2
                  elif .severity == "LOW" then 3
                  else 4 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

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

      - name: Send Slack notification
        if: steps.process_results.outputs.vulnerabilities_found == 'true'
        env:
          SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }}
          IMAGE_REF: ${{ inputs.image_ref }}
        run: |
          node .github/scripts/slack/notify.mjs \
            --channel "$SLACK_CHANNEL_ID" \
            --text "🚨 Trivy Scan: ${{ steps.process_results.outputs.critical_count }} Critical, ${{ steps.process_results.outputs.high_count }} High, ${{ steps.process_results.outputs.medium_count }} Medium, ${{ steps.process_results.outputs.low_count }} Low vulnerabilities found in $IMAGE_REF" \
            --blocks trivy \
            --results trivy-results.json \
            --image-ref "$IMAGE_REF"

