From 552b0ad82f9ddc7ff4dfee7436bfb0ce4cbee218 Mon Sep 17 00:00:00 2001 From: Sakthi Prakash K Date: Mon, 19 Jan 2026 13:29:31 +0530 Subject: [PATCH] QAO-314 | Enforce PR Failure on Critical/High Snyk Issues --- .github/workflows/snyk-sca-scan.yml | 948 ++++++++++++++++++++++++++++ README.md | 109 +++- 2 files changed, 1056 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/snyk-sca-scan.yml diff --git a/.github/workflows/snyk-sca-scan.yml b/.github/workflows/snyk-sca-scan.yml new file mode 100644 index 0000000..f048bd6 --- /dev/null +++ b/.github/workflows/snyk-sca-scan.yml @@ -0,0 +1,948 @@ +name: Source Composition Analysis Scan +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +env: + # Threshold for maximum number of critical severity issues allowed + # Can be overridden via repository variable MAX_CRITICAL_ISSUES or defaults to 0 + MAX_CRITICAL_ISSUES: ${{ vars.MAX_CRITICAL_ISSUES || '1' }} + # Threshold for maximum number of high severity issues allowed + # Can be overridden via repository variable MAX_HIGH_ISSUES or defaults to 0 + MAX_HIGH_ISSUES: ${{ vars.MAX_HIGH_ISSUES || '1' }} + # Threshold for maximum number of medium severity issues allowed + # Can be overridden via repository variable MAX_MEDIUM_ISSUES or defaults to 0 + MAX_MEDIUM_ISSUES: ${{ vars.MAX_MEDIUM_ISSUES || '500' }} + # Threshold for maximum number of low severity issues allowed + # Can be overridden via repository variable MAX_LOW_ISSUES or defaults to 0 + MAX_LOW_ISSUES: ${{ vars.MAX_LOW_ISSUES || '1000' }} + + # SLA thresholds (days since publication) for vulnerabilities WITH fixes + SLA_CRITICAL_WITH_FIX: ${{ vars.SLA_CRITICAL_WITH_FIX || '15' }} + SLA_HIGH_WITH_FIX: ${{ vars.SLA_HIGH_WITH_FIX || '30' }} + SLA_MEDIUM_WITH_FIX: ${{ vars.SLA_MEDIUM_WITH_FIX || '90' }} + SLA_LOW_WITH_FIX: ${{ vars.SLA_LOW_WITH_FIX || '180' }} + + # SLA thresholds (days since publication) for vulnerabilities WITHOUT fixes + SLA_CRITICAL_NO_FIX: ${{ vars.SLA_CRITICAL_NO_FIX || '30' }} + SLA_HIGH_NO_FIX: ${{ vars.SLA_HIGH_NO_FIX || '120' }} + SLA_MEDIUM_NO_FIX: ${{ vars.SLA_MEDIUM_NO_FIX || '365' }} + SLA_LOW_NO_FIX: ${{ vars.SLA_LOW_NO_FIX || '365' }} + +jobs: + security-sca: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Run Snyk to check for vulnerabilities + id: snyk-test + continue-on-error: true + run: | + npm install -g snyk + snyk auth ${{ secrets.SNYK_TOKEN }} + snyk test --json --all-projects > snyk-results.json || true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + + - name: Check for wildcard or latest versions in dependencies + id: check-versions + run: | + echo "===================================" + echo "Checking for wildcard/latest versions" + echo "===================================" + + # Check for * or latest in package.json + wildcard_deps=$(jq -r '.dependencies, .devDependencies | to_entries[] | select(.value == "*" or .value == "latest") | "\(.key): \(.value)"' package.json 2>/dev/null || echo "") + + if [ ! -z "$wildcard_deps" ]; then + echo "❌ Found dependencies with wildcard (*) or 'latest' versions:" + echo "$wildcard_deps" + echo "wildcard_found=true" >> $GITHUB_OUTPUT + echo "$wildcard_deps" > wildcard-deps.txt + else + echo "✅ No wildcard or 'latest' versions found" + echo "wildcard_found=false" >> $GITHUB_OUTPUT + fi + + - name: Analyze Snyk results for severity thresholds + id: analyze-results + run: | + # Install jq for JSON parsing + sudo apt-get update -qq > /dev/null 2>&1 && sudo apt-get install -y -qq jq > /dev/null 2>&1 + + echo "===================================" + echo "Analyzing Snyk results..." + echo "===================================" + + # Validate and set thresholds from environment variables + MAX_CRITICAL_ISSUES="${{ env.MAX_CRITICAL_ISSUES }}" + MAX_HIGH_ISSUES="${{ env.MAX_HIGH_ISSUES }}" + MAX_MEDIUM_ISSUES="${{ env.MAX_MEDIUM_ISSUES }}" + MAX_LOW_ISSUES="${{ env.MAX_LOW_ISSUES }}" + + # Validate thresholds are numeric + if ! [[ "$MAX_CRITICAL_ISSUES" =~ ^[0-9]+$ ]]; then + echo "Error: MAX_CRITICAL_ISSUES must be a non-negative integer. Got: $MAX_CRITICAL_ISSUES" + exit 1 + fi + + if ! [[ "$MAX_HIGH_ISSUES" =~ ^[0-9]+$ ]]; then + echo "Error: MAX_HIGH_ISSUES must be a non-negative integer. Got: $MAX_HIGH_ISSUES" + exit 1 + fi + + if ! [[ "$MAX_MEDIUM_ISSUES" =~ ^[0-9]+$ ]]; then + echo "Error: MAX_MEDIUM_ISSUES must be a non-negative integer. Got: $MAX_MEDIUM_ISSUES" + exit 1 + fi + + if ! [[ "$MAX_LOW_ISSUES" =~ ^[0-9]+$ ]]; then + echo "Error: MAX_LOW_ISSUES must be a non-negative integer. Got: $MAX_LOW_ISSUES" + exit 1 + fi + + echo "Thresholds configured:" + echo " - MAX_CRITICAL_ISSUES: $MAX_CRITICAL_ISSUES" + echo " - MAX_HIGH_ISSUES: $MAX_HIGH_ISSUES" + echo " - MAX_MEDIUM_ISSUES: $MAX_MEDIUM_ISSUES" + echo " - MAX_LOW_ISSUES: $MAX_LOW_ISSUES" + echo "" + + # Initialize counters + critical_count=0 + high_count=0 + medium_count=0 + low_count=0 + wildcard_found="${{ steps.check-versions.outputs.wildcard_found }}" + wildcard_details="" + + # Check if results file exists + if [ ! -f snyk-results.json ]; then + echo "Error: Snyk results file not found" + exit 1 + fi + + # Parse JSON and count severities - ONLY count vulnerabilities with fixes available + critical_count=$(jq -r '[.vulnerabilities[]? | select(.severity == "critical" and (.isUpgradable == true or .isPatchable == true))] | length' snyk-results.json 2>/dev/null || echo 0) + high_count=$(jq -r '[.vulnerabilities[]? | select(.severity == "high" and (.isUpgradable == true or .isPatchable == true))] | length' snyk-results.json 2>/dev/null || echo 0) + medium_count=$(jq -r '[.vulnerabilities[]? | select(.severity == "medium" and (.isUpgradable == true or .isPatchable == true))] | length' snyk-results.json 2>/dev/null || echo 0) + low_count=$(jq -r '[.vulnerabilities[]? | select(.severity == "low" and (.isUpgradable == true or .isPatchable == true))] | length' snyk-results.json 2>/dev/null || echo 0) + + # Count vulnerabilities without fixes + critical_no_fix=$(jq -r '[.vulnerabilities[]? | select(.severity == "critical" and (.isUpgradable != true and .isPatchable != true))] | length' snyk-results.json 2>/dev/null || echo 0) + high_no_fix=$(jq -r '[.vulnerabilities[]? | select(.severity == "high" and (.isUpgradable != true and .isPatchable != true))] | length' snyk-results.json 2>/dev/null || echo 0) + medium_no_fix=$(jq -r '[.vulnerabilities[]? | select(.severity == "medium" and (.isUpgradable != true and .isPatchable != true))] | length' snyk-results.json 2>/dev/null || echo 0) + low_no_fix=$(jq -r '[.vulnerabilities[]? | select(.severity == "low" and (.isUpgradable != true and .isPatchable != true))] | length' snyk-results.json 2>/dev/null || echo 0) + + # Export counts to GITHUB_ENV for next steps + echo "critical_count=$critical_count" >> $GITHUB_ENV + echo "critical_no_fix=$critical_no_fix" >> $GITHUB_ENV + echo "high_count=$high_count" >> $GITHUB_ENV + echo "high_no_fix=$high_no_fix" >> $GITHUB_ENV + echo "medium_count=$medium_count" >> $GITHUB_ENV + echo "medium_no_fix=$medium_no_fix" >> $GITHUB_ENV + echo "low_count=$low_count" >> $GITHUB_ENV + echo "low_no_fix=$low_no_fix" >> $GITHUB_ENV + echo "MAX_CRITICAL_ISSUES=$MAX_CRITICAL_ISSUES" >> $GITHUB_ENV + echo "MAX_HIGH_ISSUES=$MAX_HIGH_ISSUES" >> $GITHUB_ENV + echo "MAX_MEDIUM_ISSUES=$MAX_MEDIUM_ISSUES" >> $GITHUB_ENV + echo "MAX_LOW_ISSUES=$MAX_LOW_ISSUES" >> $GITHUB_ENV + echo "wildcard_found=$wildcard_found" >> $GITHUB_ENV + + if [ -f wildcard-deps.txt ]; then + wildcard_details=$(cat wildcard-deps.txt) + echo "wildcard_details<> $GITHUB_ENV + echo "$wildcard_details" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + fi + + - name: Calculate SLA breaches + id: calculate-sla + run: | + echo "===================================" + echo "Calculating SLA breaches..." + echo "===================================" + + # Get current time in epoch seconds + current_time=$(date +%s) + + # Load SLA thresholds + SLA_CRITICAL_WITH_FIX="${{ env.SLA_CRITICAL_WITH_FIX }}" + SLA_HIGH_WITH_FIX="${{ env.SLA_HIGH_WITH_FIX }}" + SLA_MEDIUM_WITH_FIX="${{ env.SLA_MEDIUM_WITH_FIX }}" + SLA_LOW_WITH_FIX="${{ env.SLA_LOW_WITH_FIX }}" + SLA_CRITICAL_NO_FIX="${{ env.SLA_CRITICAL_NO_FIX }}" + SLA_HIGH_NO_FIX="${{ env.SLA_HIGH_NO_FIX }}" + SLA_MEDIUM_NO_FIX="${{ env.SLA_MEDIUM_NO_FIX }}" + SLA_LOW_NO_FIX="${{ env.SLA_LOW_NO_FIX }}" + + # Validate SLA thresholds are numeric + for sla_var in "$SLA_CRITICAL_WITH_FIX" "$SLA_HIGH_WITH_FIX" "$SLA_MEDIUM_WITH_FIX" "$SLA_LOW_WITH_FIX" "$SLA_CRITICAL_NO_FIX" "$SLA_HIGH_NO_FIX" "$SLA_MEDIUM_NO_FIX" "$SLA_LOW_NO_FIX"; do + if ! [[ "$sla_var" =~ ^[0-9]+$ ]]; then + echo "Error: SLA threshold must be a non-negative integer. Got: $sla_var" + exit 1 + fi + done + + echo "SLA Thresholds (days):" + echo " Critical - With Fix: $SLA_CRITICAL_WITH_FIX, No Fix: $SLA_CRITICAL_NO_FIX" + echo " High - With Fix: $SLA_HIGH_WITH_FIX, No Fix: $SLA_HIGH_NO_FIX" + echo " Medium - With Fix: $SLA_MEDIUM_WITH_FIX, No Fix: $SLA_MEDIUM_NO_FIX" + echo " Low - With Fix: $SLA_LOW_WITH_FIX, No Fix: $SLA_LOW_NO_FIX" + echo "" + + # Helper function to count SLA breaches + count_sla_breaches() { + local severity=$1 + local has_fix=$2 + local threshold=$3 + + if [ "$has_fix" = "true" ]; then + jq --arg current "$current_time" --arg threshold "$threshold" --arg sev "$severity" -r ' + [.vulnerabilities[]? | + select(.severity == $sev and (.isUpgradable == true or .isPatchable == true)) | + select(.publicationTime != null) | + select(($current | tonumber) - (.publicationTime | fromdateiso8601) > ($threshold | tonumber * 86400))] | + length' snyk-results.json 2>/dev/null || echo 0 + else + jq --arg current "$current_time" --arg threshold "$threshold" --arg sev "$severity" -r ' + [.vulnerabilities[]? | + select(.severity == $sev and (.isUpgradable != true and .isPatchable != true)) | + select(.publicationTime != null) | + select(($current | tonumber) - (.publicationTime | fromdateiso8601) > ($threshold | tonumber * 86400))] | + length' snyk-results.json 2>/dev/null || echo 0 + fi + } + + # Count SLA breaches using helper function + critical_sla_breaches=$(count_sla_breaches "critical" "true" "$SLA_CRITICAL_WITH_FIX") + high_sla_breaches=$(count_sla_breaches "high" "true" "$SLA_HIGH_WITH_FIX") + medium_sla_breaches=$(count_sla_breaches "medium" "true" "$SLA_MEDIUM_WITH_FIX") + low_sla_breaches=$(count_sla_breaches "low" "true" "$SLA_LOW_WITH_FIX") + + critical_sla_breaches_no_fix=$(count_sla_breaches "critical" "false" "$SLA_CRITICAL_NO_FIX") + high_sla_breaches_no_fix=$(count_sla_breaches "high" "false" "$SLA_HIGH_NO_FIX") + medium_sla_breaches_no_fix=$(count_sla_breaches "medium" "false" "$SLA_MEDIUM_NO_FIX") + low_sla_breaches_no_fix=$(count_sla_breaches "low" "false" "$SLA_LOW_NO_FIX") + + echo "SLA Breaches Found:" + echo " Critical - With Fix: $critical_sla_breaches, No Fix: $critical_sla_breaches_no_fix" + echo " High - With Fix: $high_sla_breaches, No Fix: $high_sla_breaches_no_fix" + echo " Medium - With Fix: $medium_sla_breaches, No Fix: $medium_sla_breaches_no_fix" + echo " Low - With Fix: $low_sla_breaches, No Fix: $low_sla_breaches_no_fix" + echo "" + + # Export SLA counts to GITHUB_ENV + echo "critical_sla_breaches=$critical_sla_breaches" >> $GITHUB_ENV + echo "critical_sla_breaches_no_fix=$critical_sla_breaches_no_fix" >> $GITHUB_ENV + echo "high_sla_breaches=$high_sla_breaches" >> $GITHUB_ENV + echo "high_sla_breaches_no_fix=$high_sla_breaches_no_fix" >> $GITHUB_ENV + echo "medium_sla_breaches=$medium_sla_breaches" >> $GITHUB_ENV + echo "medium_sla_breaches_no_fix=$medium_sla_breaches_no_fix" >> $GITHUB_ENV + echo "low_sla_breaches=$low_sla_breaches" >> $GITHUB_ENV + echo "low_sla_breaches_no_fix=$low_sla_breaches_no_fix" >> $GITHUB_ENV + echo "SLA_CRITICAL_WITH_FIX=$SLA_CRITICAL_WITH_FIX" >> $GITHUB_ENV + echo "SLA_HIGH_WITH_FIX=$SLA_HIGH_WITH_FIX" >> $GITHUB_ENV + echo "SLA_MEDIUM_WITH_FIX=$SLA_MEDIUM_WITH_FIX" >> $GITHUB_ENV + echo "SLA_LOW_WITH_FIX=$SLA_LOW_WITH_FIX" >> $GITHUB_ENV + echo "SLA_CRITICAL_NO_FIX=$SLA_CRITICAL_NO_FIX" >> $GITHUB_ENV + echo "SLA_HIGH_NO_FIX=$SLA_HIGH_NO_FIX" >> $GITHUB_ENV + echo "SLA_MEDIUM_NO_FIX=$SLA_MEDIUM_NO_FIX" >> $GITHUB_ENV + echo "SLA_LOW_NO_FIX=$SLA_LOW_NO_FIX" >> $GITHUB_ENV + + - name: Check thresholds and generate summary + id: check-thresholds + run: | + # Get values from environment + critical_count=${{ env.critical_count }} + critical_no_fix=${{ env.critical_no_fix }} + high_count=${{ env.high_count }} + high_no_fix=${{ env.high_no_fix }} + medium_count=${{ env.medium_count }} + medium_no_fix=${{ env.medium_no_fix }} + low_count=${{ env.low_count }} + low_no_fix=${{ env.low_no_fix }} + wildcard_found=${{ env.wildcard_found }} + + MAX_CRITICAL_ISSUES=${{ env.MAX_CRITICAL_ISSUES }} + MAX_HIGH_ISSUES=${{ env.MAX_HIGH_ISSUES }} + MAX_MEDIUM_ISSUES=${{ env.MAX_MEDIUM_ISSUES }} + MAX_LOW_ISSUES=${{ env.MAX_LOW_ISSUES }} + + critical_sla_breaches=${{ env.critical_sla_breaches }} + critical_sla_breaches_no_fix=${{ env.critical_sla_breaches_no_fix }} + high_sla_breaches=${{ env.high_sla_breaches }} + high_sla_breaches_no_fix=${{ env.high_sla_breaches_no_fix }} + medium_sla_breaches=${{ env.medium_sla_breaches }} + medium_sla_breaches_no_fix=${{ env.medium_sla_breaches_no_fix }} + low_sla_breaches=${{ env.low_sla_breaches }} + low_sla_breaches_no_fix=${{ env.low_sla_breaches_no_fix }} + + SLA_CRITICAL_WITH_FIX=${{ env.SLA_CRITICAL_WITH_FIX }} + SLA_HIGH_WITH_FIX=${{ env.SLA_HIGH_WITH_FIX }} + SLA_MEDIUM_WITH_FIX=${{ env.SLA_MEDIUM_WITH_FIX }} + SLA_LOW_WITH_FIX=${{ env.SLA_LOW_WITH_FIX }} + SLA_CRITICAL_NO_FIX=${{ env.SLA_CRITICAL_NO_FIX }} + SLA_HIGH_NO_FIX=${{ env.SLA_HIGH_NO_FIX }} + SLA_MEDIUM_NO_FIX=${{ env.SLA_MEDIUM_NO_FIX }} + SLA_LOW_NO_FIX=${{ env.SLA_LOW_NO_FIX }} + + # Get wildcard details if exists + wildcard_details="${{ env.wildcard_details }}" + + # Output results + echo "" + echo "===================================" + echo "Security Scan Results Summary" + echo "===================================" + echo "Critical issues with fixes: $critical_count (Max allowed: $MAX_CRITICAL_ISSUES)" + echo "Critical issues without fixes (excluded): $critical_no_fix" + echo "High issues with fixes: $high_count (Max allowed: $MAX_HIGH_ISSUES)" + echo "High issues without fixes (excluded): $high_no_fix" + echo "Medium issues with fixes: $medium_count (Max allowed: $MAX_MEDIUM_ISSUES)" + echo "Medium issues without fixes (excluded): $medium_no_fix" + echo "Low issues with fixes: $low_count (Max allowed: $MAX_LOW_ISSUES)" + echo "Low issues without fixes (excluded): $low_no_fix" + echo "Wildcard/Latest versions found: $wildcard_found" + echo "" + echo "ℹ️ Note: Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds." + echo "" + echo "===================================" + echo "SLA Breach Summary (Days Since Publication)" + echo "===================================" + echo "Critical SLA breaches - With fixes: $critical_sla_breaches (Threshold: $SLA_CRITICAL_WITH_FIX days)" + echo "Critical SLA breaches - No fixes: $critical_sla_breaches_no_fix (Threshold: $SLA_CRITICAL_NO_FIX days)" + echo "High SLA breaches - With fixes: $high_sla_breaches (Threshold: $SLA_HIGH_WITH_FIX days)" + echo "High SLA breaches - No fixes: $high_sla_breaches_no_fix (Threshold: $SLA_HIGH_NO_FIX days)" + echo "Medium SLA breaches - With fixes: $medium_sla_breaches (Threshold: $SLA_MEDIUM_WITH_FIX days)" + echo "Medium SLA breaches - No fixes: $medium_sla_breaches_no_fix (Threshold: $SLA_MEDIUM_NO_FIX days)" + echo "Low SLA breaches - With fixes: $low_sla_breaches (Threshold: $SLA_LOW_WITH_FIX days)" + echo "Low SLA breaches - No fixes: $low_sla_breaches_no_fix (Threshold: $SLA_LOW_NO_FIX days)" + echo "" + + # Check thresholds + fail_build=false + failure_reasons="" + + if [ "$critical_count" -gt "$MAX_CRITICAL_ISSUES" ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ CRITICAL SEVERITY THRESHOLD BREACHED: Found $critical_count critical issues (max allowed: $MAX_CRITICAL_ISSUES)\n" + fi + + if [ "$high_count" -gt "$MAX_HIGH_ISSUES" ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ HIGH SEVERITY THRESHOLD BREACHED: Found $high_count high issues (max allowed: $MAX_HIGH_ISSUES)\n" + fi + + if [ "$medium_count" -gt "$MAX_MEDIUM_ISSUES" ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ MEDIUM SEVERITY THRESHOLD BREACHED: Found $medium_count medium issues (max allowed: $MAX_MEDIUM_ISSUES)\n" + fi + + if [ "$low_count" -gt "$MAX_LOW_ISSUES" ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ LOW SEVERITY THRESHOLD BREACHED: Found $low_count low issues (max allowed: $MAX_LOW_ISSUES)\n" + fi + + # Check SLA breaches (with fixes) + if [ "$critical_sla_breaches" -gt 0 ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ CRITICAL SLA BREACH (WITH FIXES): $critical_sla_breaches issues exceed $SLA_CRITICAL_WITH_FIX days since publication\n" + fi + + if [ "$high_sla_breaches" -gt 0 ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ HIGH SLA BREACH (WITH FIXES): $high_sla_breaches issues exceed $SLA_HIGH_WITH_FIX days since publication\n" + fi + + if [ "$medium_sla_breaches" -gt 0 ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ MEDIUM SLA BREACH (WITH FIXES): $medium_sla_breaches issues exceed $SLA_MEDIUM_WITH_FIX days since publication\n" + fi + + if [ "$low_sla_breaches" -gt 0 ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ LOW SLA BREACH (WITH FIXES): $low_sla_breaches issues exceed $SLA_LOW_WITH_FIX days since publication\n" + fi + + # Check SLA breaches (without fixes) + if [ "$critical_sla_breaches_no_fix" -gt 0 ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ CRITICAL SLA BREACH (NO FIXES): $critical_sla_breaches_no_fix issues exceed $SLA_CRITICAL_NO_FIX days since publication\n" + fi + + if [ "$high_sla_breaches_no_fix" -gt 0 ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ HIGH SLA BREACH (NO FIXES): $high_sla_breaches_no_fix issues exceed $SLA_HIGH_NO_FIX days since publication\n" + fi + + if [ "$medium_sla_breaches_no_fix" -gt 0 ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ MEDIUM SLA BREACH (NO FIXES): $medium_sla_breaches_no_fix issues exceed $SLA_MEDIUM_NO_FIX days since publication\n" + fi + + if [ "$low_sla_breaches_no_fix" -gt 0 ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ LOW SLA BREACH (NO FIXES): $low_sla_breaches_no_fix issues exceed $SLA_LOW_NO_FIX days since publication\n" + fi + + # Show informational message about vulnerabilities without fixes + if [ "$critical_no_fix" -gt 0 ] || [ "$high_no_fix" -gt 0 ] || [ "$medium_no_fix" -gt 0 ] || [ "$low_no_fix" -gt 0 ]; then + echo "" + echo "ℹ️ Vulnerabilities without fixes (excluded from thresholds):" + echo " - Critical: $critical_no_fix" + echo " - High: $high_no_fix" + echo " - Medium: $medium_no_fix" + echo " - Low: $low_no_fix" + fi + + if [ "$wildcard_found" = "true" ]; then + fail_build=true + failure_reasons="${failure_reasons}❌ WILDCARD/LATEST VERSIONS DETECTED: Dependencies using '*' or 'latest' versions found\n" + echo "" + echo "Wildcard/Latest Version Dependencies:" + echo "------------------------------------------------" + echo "$wildcard_details" + fi + + # Export fail_build for later steps + echo "fail_build=$fail_build" >> $GITHUB_ENV + + # Create summary for GitHub + echo "### 🔒 Security Scan Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "> ℹ️ **Note:** Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Check Type | Count (with fixes) | Without fixes | Threshold | Result |" >> $GITHUB_STEP_SUMMARY + echo "|------------|-------------------|---------------|-----------|--------|" >> $GITHUB_STEP_SUMMARY + + if [ "$critical_count" -gt "$MAX_CRITICAL_ISSUES" ]; then + echo "| 🔴 Critical Severity | $critical_count | $critical_no_fix | $MAX_CRITICAL_ISSUES | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🔴 Critical Severity | $critical_count | $critical_no_fix | $MAX_CRITICAL_ISSUES | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$high_count" -gt "$MAX_HIGH_ISSUES" ]; then + echo "| 🟠 High Severity | $high_count | $high_no_fix | $MAX_HIGH_ISSUES | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🟠 High Severity | $high_count | $high_no_fix | $MAX_HIGH_ISSUES | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$medium_count" -gt "$MAX_MEDIUM_ISSUES" ]; then + echo "| 🟡 Medium Severity | $medium_count | $medium_no_fix | $MAX_MEDIUM_ISSUES | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🟡 Medium Severity | $medium_count | $medium_no_fix | $MAX_MEDIUM_ISSUES | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$low_count" -gt "$MAX_LOW_ISSUES" ]; then + echo "| 🔵 Low Severity | $low_count | $low_no_fix | $MAX_LOW_ISSUES | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🔵 Low Severity | $low_count | $low_no_fix | $MAX_LOW_ISSUES | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$wildcard_found" = "true" ]; then + echo "| ⚡ Wildcard/Latest Versions | Detected | Not Allowed | ❌ Failed |" >> $GITHUB_STEP_SUMMARY + else + echo "| ⚡ Wildcard/Latest Versions | None | Not Allowed | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + + # Add SLA Breach Summary section + total_sla_breaches=$((critical_sla_breaches + high_sla_breaches + medium_sla_breaches + low_sla_breaches + critical_sla_breaches_no_fix + high_sla_breaches_no_fix + medium_sla_breaches_no_fix + low_sla_breaches_no_fix)) + + if [ "$total_sla_breaches" -gt 0 ]; then + echo "### ⏱️ SLA Breach Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "> ⚠️ **Warning:** The following vulnerabilities have exceeded their SLA thresholds (days since publication)." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Severity | Breaches (with fixes) | Breaches (no fixes) | SLA Threshold (with/no fixes) | Status |" >> $GITHUB_STEP_SUMMARY + echo "|----------|----------------------|---------------------|------------------------------|--------|" >> $GITHUB_STEP_SUMMARY + + if [ "$critical_sla_breaches" -gt 0 ] || [ "$critical_sla_breaches_no_fix" -gt 0 ]; then + critical_sla_status="❌ Failed" + echo "| 🔴 Critical | $critical_sla_breaches | $critical_sla_breaches_no_fix | $SLA_CRITICAL_WITH_FIX / $SLA_CRITICAL_NO_FIX days | $critical_sla_status |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🔴 Critical | 0 | 0 | $SLA_CRITICAL_WITH_FIX / $SLA_CRITICAL_NO_FIX days | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$high_sla_breaches" -gt 0 ] || [ "$high_sla_breaches_no_fix" -gt 0 ]; then + high_sla_status="❌ Failed" + echo "| 🟠 High | $high_sla_breaches | $high_sla_breaches_no_fix | $SLA_HIGH_WITH_FIX / $SLA_HIGH_NO_FIX days | $high_sla_status |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🟠 High | 0 | 0 | $SLA_HIGH_WITH_FIX / $SLA_HIGH_NO_FIX days | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$medium_sla_breaches" -gt 0 ] || [ "$medium_sla_breaches_no_fix" -gt 0 ]; then + medium_sla_status="❌ Failed" + echo "| 🟡 Medium | $medium_sla_breaches | $medium_sla_breaches_no_fix | $SLA_MEDIUM_WITH_FIX / $SLA_MEDIUM_NO_FIX days | $medium_sla_status |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🟡 Medium | 0 | 0 | $SLA_MEDIUM_WITH_FIX / $SLA_MEDIUM_NO_FIX days | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$low_sla_breaches" -gt 0 ] || [ "$low_sla_breaches_no_fix" -gt 0 ]; then + low_sla_status="❌ Failed" + echo "| 🔵 Low | $low_sla_breaches | $low_sla_breaches_no_fix | $SLA_LOW_WITH_FIX / $SLA_LOW_NO_FIX days | $low_sla_status |" >> $GITHUB_STEP_SUMMARY + else + echo "| 🔵 Low | 0 | 0 | $SLA_LOW_WITH_FIX / $SLA_LOW_NO_FIX days | ✅ Passed |" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Add informational section for vulnerabilities without fixes + if [ "$critical_no_fix" -gt 0 ] || [ "$high_no_fix" -gt 0 ] || [ "$medium_no_fix" -gt 0 ] || [ "$low_no_fix" -gt 0 ]; then + echo "### ℹ️ Vulnerabilities Without Available Fixes (Informational Only)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The following vulnerabilities were detected but **do not have fixes available** (no upgrade or patch). These are excluded from failure thresholds:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- Critical without fixes: **$critical_no_fix**" >> $GITHUB_STEP_SUMMARY + echo "- High without fixes: **$high_no_fix**" >> $GITHUB_STEP_SUMMARY + echo "- Medium without fixes: **$medium_no_fix**" >> $GITHUB_STEP_SUMMARY + echo "- Low without fixes: **$low_no_fix**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "> 💡 These vulnerabilities are monitored but won't fail the build since no fixes are currently available." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$wildcard_found" = "true" ]; then + echo "### 🔧 Dependencies with Wildcard/Latest Versions" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The following dependencies are using wildcard (\`*\`) or \`latest\` versions, which can lead to unpredictable builds and security issues:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$wildcard_details" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Action Required:** Pin all dependencies to specific versions for security and reproducibility." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # Fail if any threshold is breached + if [ "$fail_build" = true ]; then + echo "" + echo "===================================" + echo "❌ BUILD FAILED - Security checks failed" + echo "===================================" + echo -e "$failure_reasons" + echo "### ❌ Build Failed - Security Issues Detected" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo -e "$failure_reasons" >> $GITHUB_STEP_SUMMARY + + exit 1 + else + echo "" + echo "===================================" + echo "✅ BUILD PASSED - All security checks passed" + echo "===================================" + echo "### ✅ Build Passed - All Security Checks Passed" >> $GITHUB_STEP_SUMMARY + fi + + - name: Upload Snyk results as artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: snyk-results + path: snyk-results.json + retention-days: 30 + + - name: Comment PR with Security Scan Results + if: always() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const criticalCount = process.env.critical_count || '0'; + const criticalNoFix = process.env.critical_no_fix || '0'; + const highCount = process.env.high_count || '0'; + const highNoFix = process.env.high_no_fix || '0'; + const mediumCount = process.env.medium_count || '0'; + const mediumNoFix = process.env.medium_no_fix || '0'; + const lowCount = process.env.low_count || '0'; + const lowNoFix = process.env.low_no_fix || '0'; + const maxCritical = process.env.MAX_CRITICAL_ISSUES || '0'; + const maxHigh = process.env.MAX_HIGH_ISSUES || '0'; + const maxMedium = process.env.MAX_MEDIUM_ISSUES || '0'; + const maxLow = process.env.MAX_LOW_ISSUES || '0'; + const wildcardFound = process.env.WILDCARD_FOUND || 'false'; + const failBuild = process.env.fail_build || 'false'; + + // Determine status for each check + const criticalStatus = parseInt(criticalCount) > parseInt(maxCritical) ? '❌ Failed' : '✅ Passed'; + const highStatus = parseInt(highCount) > parseInt(maxHigh) ? '❌ Failed' : '✅ Passed'; + const mediumStatus = parseInt(mediumCount) > parseInt(maxMedium) ? '❌ Failed' : '✅ Passed'; + const lowStatus = parseInt(lowCount) > parseInt(maxLow) ? '❌ Failed' : '✅ Passed'; + const wildcardDisplay = wildcardFound === 'true' ? 'Detected' : 'None'; + const wildcardStatus = wildcardFound === 'true' ? '❌ Failed' : '✅ Passed'; + + // Build comment + let comment = '### 🔒 Security Scan Results\n\n'; + comment += '> ℹ️ **Note:** Only vulnerabilities with available fixes (upgrades or patches) are counted toward thresholds.\n\n'; + comment += '| Check Type | Count (with fixes) | Without fixes | Threshold | Result |\n'; + comment += '|------------|-------------------|---------------|-----------|--------|\n'; + comment += `| 🔴 Critical Severity | ${criticalCount} | ${criticalNoFix} | ${maxCritical} | ${criticalStatus} |\n`; + comment += `| 🟠 High Severity | ${highCount} | ${highNoFix} | ${maxHigh} | ${highStatus} |\n`; + comment += `| 🟡 Medium Severity | ${mediumCount} | ${mediumNoFix} | ${maxMedium} | ${mediumStatus} |\n`; + comment += `| 🔵 Low Severity | ${lowCount} | ${lowNoFix} | ${maxLow} | ${lowStatus} |\n`; + comment += `| ⚡ Wildcard/Latest Versions | ${wildcardDisplay} | - | Not Allowed | ${wildcardStatus} |\n\n`; + + // Get SLA breach data + const criticalSlaBreaches = parseInt(process.env.critical_sla_breaches || '0'); + const criticalSlaBreachesNoFix = parseInt(process.env.critical_sla_breaches_no_fix || '0'); + const highSlaBreaches = parseInt(process.env.high_sla_breaches || '0'); + const highSlaBreachesNoFix = parseInt(process.env.high_sla_breaches_no_fix || '0'); + const mediumSlaBreaches = parseInt(process.env.medium_sla_breaches || '0'); + const mediumSlaBreachesNoFix = parseInt(process.env.medium_sla_breaches_no_fix || '0'); + const lowSlaBreaches = parseInt(process.env.low_sla_breaches || '0'); + const lowSlaBreachesNoFix = parseInt(process.env.low_sla_breaches_no_fix || '0'); + + const slaCriticalWithFix = process.env.SLA_CRITICAL_WITH_FIX || '15'; + const slaHighWithFix = process.env.SLA_HIGH_WITH_FIX || '90'; + const slaMediumWithFix = process.env.SLA_MEDIUM_WITH_FIX || '120'; + const slaLowWithFix = process.env.SLA_LOW_WITH_FIX || '180'; + const slaCriticalNoFix = process.env.SLA_CRITICAL_NO_FIX || '30'; + const slaHighNoFix = process.env.SLA_HIGH_NO_FIX || '120'; + const slaMediumNoFix = process.env.SLA_MEDIUM_NO_FIX || '365'; + const slaLowNoFix = process.env.SLA_LOW_NO_FIX || '365'; + + const totalSlaBreaches = criticalSlaBreaches + criticalSlaBreachesNoFix + highSlaBreaches + highSlaBreachesNoFix + mediumSlaBreaches + mediumSlaBreachesNoFix + lowSlaBreaches + lowSlaBreachesNoFix; + + // Add SLA Breach section if there are any breaches + if (totalSlaBreaches > 0) { + comment += '### ⏱️ SLA Breach Alert\n\n'; + comment += '> ⚠️ **Warning:** The following vulnerabilities have exceeded their SLA thresholds (days since publication by Snyk).\n\n'; + comment += '**SLA Thresholds:**\n'; + comment += `- 🔴 Critical: ${slaCriticalWithFix} days (with fix) / ${slaCriticalNoFix} days (without fix)\n`; + comment += `- 🟠 High: ${slaHighWithFix} days (with fix) / ${slaHighNoFix} days (without fix)\n`; + comment += `- 🟡 Medium: ${slaMediumWithFix} days (with fix) / ${slaMediumNoFix} days (without fix)\n`; + comment += `- 🔵 Low: ${slaLowWithFix} days (with fix) / ${slaLowNoFix} days (without fix)\n\n`; + comment += '| Vulnerability | With Fix | Without Fix | Result |\n'; + comment += '|---------------|----------|-------------|--------|\n'; + + const criticalSlaStatus = (criticalSlaBreaches > 0 || criticalSlaBreachesNoFix > 0) ? '❌ Failed' : '✅ Passed'; + const highSlaStatus = (highSlaBreaches > 0 || highSlaBreachesNoFix > 0) ? '❌ Failed' : '✅ Passed'; + const mediumSlaStatus = (mediumSlaBreaches > 0 || mediumSlaBreachesNoFix > 0) ? '❌ Failed' : '✅ Passed'; + const lowSlaStatus = (lowSlaBreaches > 0 || lowSlaBreachesNoFix > 0) ? '❌ Failed' : '✅ Passed'; + + comment += `| 🔴 Critical | ${criticalSlaBreaches} | ${criticalSlaBreachesNoFix} | ${criticalSlaStatus} |\n`; + comment += `| 🟠 High | ${highSlaBreaches} | ${highSlaBreachesNoFix} | ${highSlaStatus} |\n`; + comment += `| 🟡 Medium | ${mediumSlaBreaches} | ${mediumSlaBreachesNoFix} | ${mediumSlaStatus} |\n`; + comment += `| 🔵 Low | ${lowSlaBreaches} | ${lowSlaBreachesNoFix} | ${lowSlaStatus} |\n\n`; + } + + // Read snyk-results.json to extract vulnerability details + const fs = require('fs'); + let snykData = null; + try { + const snykResults = fs.readFileSync('snyk-results.json', 'utf8'); + snykData = JSON.parse(snykResults); + } catch (error) { + console.log('Could not read snyk-results.json:', error.message); + } + + // Add detailed SLA breach listings if we have data + if (snykData && totalSlaBreaches > 0) { + const currentTime = Math.floor(Date.now() / 1000); + + // Helper function to calculate days since publication + const daysSince = (publicationTime) => { + if (!publicationTime) return null; + const pubTime = new Date(publicationTime).getTime() / 1000; + return Math.floor((currentTime - pubTime) / 86400); + }; + + // Critical SLA breaches (with fixes) + if (criticalSlaBreaches > 0) { + const slaThreshold = parseInt(slaCriticalWithFix); + const breachedVulns = snykData.vulnerabilities + .filter(v => { + const days = daysSince(v.publicationTime); + return v.severity === 'critical' && + (v.isUpgradable || v.isPatchable) && + days !== null && + days > slaThreshold; + }) + .slice(0, 5); + + if (breachedVulns.length > 0) { + comment += '#### 🔴 Critical SLA Breaches (With Fixes)\n\n'; + breachedVulns.forEach(vuln => { + const snykUrl = `https://security.snyk.io/vuln/${vuln.id}`; + const packageInfo = `${vuln.packageName}@${vuln.version}`; + const title = vuln.title || 'Security vulnerability'; + const days = daysSince(vuln.publicationTime); + + let fix = 'No direct fix available'; + if (vuln.isUpgradable && vuln.upgradePath && vuln.upgradePath.length > 1) { + const targetVersion = vuln.upgradePath[1]; + if (targetVersion && targetVersion !== vuln.packageName) { + fix = `Upgrade to \`${targetVersion}\``; + } + } else if (vuln.isPatchable) { + fix = 'Apply Snyk patch'; + } + + comment += `- **[${packageInfo}](${snykUrl})** - ${title}\n`; + comment += ` - Published: ${days} days ago (SLA: ${slaThreshold} days)\n`; + comment += ` - Fix: ${fix}\n`; + }); + + if (criticalSlaBreaches > 5) { + comment += `\n_... and ${criticalSlaBreaches - 5} more critical SLA breaches._\n`; + } + + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + comment += `\n📥 **[Download Full Report](${runUrl})** for complete SLA breach details.\n\n`; + } + } + + // High SLA breaches (with fixes) + if (highSlaBreaches > 0) { + const slaThreshold = parseInt(slaHighWithFix); + const breachedVulns = snykData.vulnerabilities + .filter(v => { + const days = daysSince(v.publicationTime); + return v.severity === 'high' && + (v.isUpgradable || v.isPatchable) && + days !== null && + days > slaThreshold; + }) + .slice(0, 5); + + if (breachedVulns.length > 0) { + comment += '#### 🟠 High SLA Breaches (With Fixes)\n\n'; + breachedVulns.forEach(vuln => { + const snykUrl = `https://security.snyk.io/vuln/${vuln.id}`; + const packageInfo = `${vuln.packageName}@${vuln.version}`; + const title = vuln.title || 'Security vulnerability'; + const days = daysSince(vuln.publicationTime); + + let fix = 'No direct fix available'; + if (vuln.isUpgradable && vuln.upgradePath && vuln.upgradePath.length > 1) { + const targetVersion = vuln.upgradePath[1]; + if (targetVersion && targetVersion !== vuln.packageName) { + fix = `Upgrade to \`${targetVersion}\``; + } + } else if (vuln.isPatchable) { + fix = 'Apply Snyk patch'; + } + + comment += `- **[${packageInfo}](${snykUrl})** - ${title}\n`; + comment += ` - Published: ${days} days ago (SLA: ${slaThreshold} days)\n`; + comment += ` - Fix: ${fix}\n`; + }); + + if (highSlaBreaches > 5) { + comment += `\n_... and ${highSlaBreaches - 5} more high SLA breaches._\n`; + } + + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + comment += `\n📥 **[Download Full Report](${runUrl})** for complete SLA breach details.\n\n`; + } + } + + // Medium SLA breaches (with fixes) + if (mediumSlaBreaches > 0) { + const slaThreshold = parseInt(slaMediumWithFix); + const breachedVulns = snykData.vulnerabilities + .filter(v => { + const days = daysSince(v.publicationTime); + return v.severity === 'medium' && + (v.isUpgradable || v.isPatchable) && + days !== null && + days > slaThreshold; + }) + .slice(0, 5); + + if (breachedVulns.length > 0) { + comment += '#### 🟡 Medium SLA Breaches (With Fixes)\n\n'; + breachedVulns.forEach(vuln => { + const snykUrl = `https://security.snyk.io/vuln/${vuln.id}`; + const packageInfo = `${vuln.packageName}@${vuln.version}`; + const title = vuln.title || 'Security vulnerability'; + const days = daysSince(vuln.publicationTime); + + let fix = 'No direct fix available'; + if (vuln.isUpgradable && vuln.upgradePath && vuln.upgradePath.length > 1) { + const targetVersion = vuln.upgradePath[1]; + if (targetVersion && targetVersion !== vuln.packageName) { + fix = `Upgrade to \`${targetVersion}\``; + } + } else if (vuln.isPatchable) { + fix = 'Apply Snyk patch'; + } + + comment += `- **[${packageInfo}](${snykUrl})** - ${title}\n`; + comment += ` - Published: ${days} days ago (SLA: ${slaThreshold} days)\n`; + comment += ` - Fix: ${fix}\n`; + }); + + if (mediumSlaBreaches > 5) { + comment += `\n_... and ${mediumSlaBreaches - 5} more medium SLA breaches._\n`; + } + + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + comment += `\n📥 **[Download Full Report](${runUrl})** for complete SLA breach details.\n\n`; + } + } + + // Low SLA breaches (with fixes) + if (lowSlaBreaches > 0) { + const slaThreshold = parseInt(slaLowWithFix); + const breachedVulns = snykData.vulnerabilities + .filter(v => { + const days = daysSince(v.publicationTime); + return v.severity === 'low' && + (v.isUpgradable || v.isPatchable) && + days !== null && + days > slaThreshold; + }) + .slice(0, 5); + + if (breachedVulns.length > 0) { + comment += '#### 🔵 Low SLA Breaches (With Fixes)\n\n'; + breachedVulns.forEach(vuln => { + const snykUrl = `https://security.snyk.io/vuln/${vuln.id}`; + const packageInfo = `${vuln.packageName}@${vuln.version}`; + const title = vuln.title || 'Security vulnerability'; + const days = daysSince(vuln.publicationTime); + + let fix = 'No direct fix available'; + if (vuln.isUpgradable && vuln.upgradePath && vuln.upgradePath.length > 1) { + const targetVersion = vuln.upgradePath[1]; + if (targetVersion && targetVersion !== vuln.packageName) { + fix = `Upgrade to \`${targetVersion}\``; + } + } else if (vuln.isPatchable) { + fix = 'Apply Snyk patch'; + } + + comment += `- **[${packageInfo}](${snykUrl})** - ${title}\n`; + comment += ` - Published: ${days} days ago (SLA: ${slaThreshold} days)\n`; + comment += ` - Fix: ${fix}\n`; + }); + + if (lowSlaBreaches > 5) { + comment += `\n_... and ${lowSlaBreaches - 5} more low SLA breaches._\n`; + } + + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + comment += `\n📥 **[Download Full Report](${runUrl})** for complete SLA breach details.\n\n`; + } + } + } + + // Add Critical Vulnerabilities section + if (snykData && parseInt(criticalCount) > 0) { + const criticalVulns = snykData.vulnerabilities + .filter(v => v.severity === 'critical' && (v.isUpgradable || v.isPatchable)) + .slice(0, 10); // Limit to 10 + + if (criticalVulns.length > 0) { + comment += '### 🔴 Critical Vulnerabilities\n\n'; + + criticalVulns.forEach(vuln => { + const snykUrl = `https://security.snyk.io/vuln/${vuln.id}`; + const packageInfo = `${vuln.packageName}@${vuln.version}`; + const title = vuln.title || 'Security vulnerability'; + + // Determine fix + let fix = 'No direct fix available'; + if (vuln.isUpgradable && vuln.upgradePath && vuln.upgradePath.length > 1) { + const targetVersion = vuln.upgradePath[1]; + if (targetVersion && targetVersion !== vuln.packageName) { + fix = `Upgrade to \`${targetVersion}\``; + } + } else if (vuln.isPatchable) { + fix = 'Apply Snyk patch'; + } + + comment += `- **[${packageInfo}](${snykUrl})** - ${title}\n`; + comment += ` - Fix: ${fix}\n`; + }); + + if (parseInt(criticalCount) > 10) { + comment += `\n_... and ${parseInt(criticalCount) - 10} more critical issues._\n`; + } + comment += '\n'; + } + } + + // Add High Vulnerabilities section + if (snykData && parseInt(highCount) > 0) { + const highVulns = snykData.vulnerabilities + .filter(v => v.severity === 'high' && (v.isUpgradable || v.isPatchable)) + .slice(0, 10); // Limit to 10 + + if (highVulns.length > 0) { + comment += '### 🟠 High Vulnerabilities\n\n'; + + highVulns.forEach(vuln => { + const snykUrl = `https://security.snyk.io/vuln/${vuln.id}`; + const packageInfo = `${vuln.packageName}@${vuln.version}`; + const title = vuln.title || 'Security vulnerability'; + + // Determine fix + let fix = 'No direct fix available'; + if (vuln.isUpgradable && vuln.upgradePath && vuln.upgradePath.length > 1) { + const targetVersion = vuln.upgradePath[1]; + if (targetVersion && targetVersion !== vuln.packageName) { + fix = `Upgrade to \`${targetVersion}\``; + } + } else if (vuln.isPatchable) { + fix = 'Apply Snyk patch'; + } + + comment += `- **[${packageInfo}](${snykUrl})** - ${title}\n`; + comment += ` - Fix: ${fix}\n`; + }); + + if (parseInt(highCount) > 10) { + comment += `\n_... and ${parseInt(highCount) - 10} more high issues._\n`; + } + comment += '\n'; + } + } + + // Add link to full report after vulnerability lists + if (snykData && (parseInt(criticalCount) > 0 || parseInt(highCount) > 0)) { + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + comment += `📥 **[Download Full Report](${runUrl})** for complete vulnerability details.\n\n`; + } + + // Add vulnerabilities without fixes section + if (parseInt(criticalNoFix) > 0 || parseInt(highNoFix) > 0 || parseInt(mediumNoFix) > 0 || parseInt(lowNoFix) > 0) { + comment += '### ℹ️ Vulnerabilities Without Available Fixes (Informational Only)\n\n'; + comment += 'The following vulnerabilities were detected but **do not have fixes available** (no upgrade or patch). These are excluded from failure thresholds:\n\n'; + comment += `- Critical without fixes: **${criticalNoFix}**\n`; + comment += `- High without fixes: **${highNoFix}**\n`; + comment += `- Medium without fixes: **${mediumNoFix}**\n`; + comment += `- Low without fixes: **${lowNoFix}**\n\n`; + comment += '> 💡 These vulnerabilities are monitored but won\'t fail the build since no fixes are currently available.\n\n'; + } + + // Final status + if (failBuild === 'true') { + comment += '### ❌ Build Failed - Security Issues Detected\n\n'; + comment += 'Please review and fix the security vulnerabilities before merging.\n'; + } else { + comment += '### ✅ All Security Checks Passed\n\n'; + comment += 'No security issues detected. Safe to merge! 🎉\n'; + } + + // Post comment + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); diff --git a/README.md b/README.md index 5e9fd8a..d75a9c1 100644 --- a/README.md +++ b/README.md @@ -1 +1,108 @@ -# contentstack-ci-workflows \ No newline at end of file +# Contentstack CI Workflows + +This repository contains reusable GitHub Actions workflows for Contentstack projects. These workflows can be easily integrated into any repository to automate CI/CD processes. + +## How to Use Workflows in Your Repository + +These workflows are designed to be used as **reusable workflows** using the `uses:` syntax. You don't need to copy the workflow files - simply reference them from this repository. + +### Step 1: Create a Workflow File in Your Repository + +Create a new workflow file in your repository at `.github/workflows/.yml`: + +```yaml +name: Code Quality Pipeline + +on: + push: + branches: [main, master, feature/*] + pull_request: + branches: [main, master] + workflow_dispatch: + +jobs: + security-scan: + name: Security Scan + uses: contentstack/contentstack-ci-workflows/.github/workflows/snyk-sca-scan.yml@main + secrets: inherit +``` + +### Step 2: Configure Required Secrets + +Go to your repository's **Settings → Secrets and variables → Actions** and add: + +- **Required Secrets**: + - `SNYK_TOKEN`: Your Snyk authentication token + + +### Step 3: Reference the Workflow + +Use the `uses:` syntax to reference the workflow from this repository: + +```yaml +jobs: + my-job: + uses: contentstack/contentstack-ci-workflows/.github/workflows/.yml@main + secrets: inherit +``` + +### Example: Complete Workflow File + +Here's a complete example of how to use the Snyk SCA scan workflow: + +```yaml +name: Security and Quality Checks + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + workflow_dispatch: + +jobs: + security-scan: + name: Snyk Security Scan + uses: contentstack/contentstack-ci-workflows/.github/workflows/snyk-sca-scan.yml@main + secrets: inherit +``` + + +## Available Workflows + +### [Source Composition Analysis Scan](.github/workflows/snyk-sca-scan.yml) + +A comprehensive security scanning workflow that uses Snyk to detect vulnerabilities in your project dependencies. This workflow: + +- **Scans all projects** in your repository for open-source vulnerabilities +- **Enforces severity thresholds** for critical, high, medium, and low severity issues +- **Tracks SLA breaches** based on days since vulnerability publication +- **Detects wildcard/latest versions** in dependencies +- **Posts detailed results** as PR comments +- **Fails builds** when security thresholds are exceeded + +**Prerequisites:** +- `package.json` (and preferably `package-lock.json`) must be present in the root directory of your repository for the wildcard version check to function properly + +**Required Secrets:** +- `SNYK_TOKEN`: Your Snyk authentication token + +**Optional Repository Variables:** +- `MAX_CRITICAL_ISSUES`: Maximum allowed critical issues (default: 1) +- `MAX_HIGH_ISSUES`: Maximum allowed high issues (default: 1) +- `MAX_MEDIUM_ISSUES`: Maximum allowed medium issues (default: 500) +- `MAX_LOW_ISSUES`: Maximum allowed low issues (default: 1000) +- `SLA_CRITICAL_WITH_FIX`: SLA threshold in days for critical issues with fixes (default: 15) +- `SLA_HIGH_WITH_FIX`: SLA threshold in days for high issues with fixes (default: 30) +- `SLA_MEDIUM_WITH_FIX`: SLA threshold in days for medium issues with fixes (default: 90) +- `SLA_LOW_WITH_FIX`: SLA threshold in days for low issues with fixes (default: 180) +- `SLA_CRITICAL_NO_FIX`: SLA threshold in days for critical issues without fixes (default: 30) +- `SLA_HIGH_NO_FIX`: SLA threshold in days for high issues without fixes (default: 120) +- `SLA_MEDIUM_NO_FIX`: SLA threshold in days for medium issues without fixes (default: 365) +- `SLA_LOW_NO_FIX`: SLA threshold in days for low issues without fixes (default: 365) + +**Triggers:** +- Pull requests (opened, synchronize, reopened) +- Manual workflow dispatch + +---