From 06f8ee3cd976260c93789a687d4ffdc1d6245422 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira da Silva Date: Tue, 4 Jun 2024 11:31:00 -0300 Subject: [PATCH] [Backport] Snyk report to identify branches impacted by a CVE Closes #29813 --- .github/scripts/snyk-report.sh | 120 ++++++++++++++++++++++++++++ .github/workflows/snyk-analysis.yml | 25 +++--- 2 files changed, 129 insertions(+), 16 deletions(-) create mode 100755 .github/scripts/snyk-report.sh diff --git a/.github/scripts/snyk-report.sh b/.github/scripts/snyk-report.sh new file mode 100755 index 000000000000..5630d9410743 --- /dev/null +++ b/.github/scripts/snyk-report.sh @@ -0,0 +1,120 @@ +#!/bin/bash -e + +KEYCLOAK_REPO="keycloak/keycloak" +BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + +# Extract the version number if BRANCH_NAME contains a slash +if [[ $BRANCH_NAME == *\/* ]]; then + BRANCH_NAME=$(echo $BRANCH_NAME | grep -oP '\d+\.\d+') +fi + +# Prevent duplicates by checking if a similar CVE ID exists +check_github_issue_exists() { + local issue_title="$1" + # Extract the CVE ID + local CVE_ID=$(echo "$issue_title" | grep -oE '(CVE-[0-9]{4}-[0-9]{4,7}|SNYK-[A-Z]+-[A-Z0-9]+-[0-9]{4,7})') + local search_url="https://api.github.com/search/issues?q=$CVE_ID+is%3Aissue+sort%3Aupdated-desc+repo:$KEYCLOAK_REPO" + local response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" "$search_url") + local count=$(echo "$response" | jq '.total_count') + + # Check for bad credentials + if printf "%s" "$response" | jq -e '.message == "Bad credentials"' > /dev/null; then + printf "Error: Bad credentials\n%s\n" "$response" + echo "Error: Bad credentials. Aborting script." + exit 1 + fi + + # Check for rate limiting + if printf "%s" "$response" | jq -e '.message == "API rate limit exceeded"' > /dev/null; then + printf "Error: API rate limit exceeded\n%s\n" "$response" + exit 1 + fi + + # Check if total_count is available + if [[ $count == "null" ]]; then + printf "Error: total_count not available in response\n%s\n" "$response" + exit 1 + fi + + if [[ $count -gt 0 ]]; then + local issue_id=$(echo "$response" | jq -r '.items[0].number') + echo "$issue_id" + else + echo "1" + fi +} + +# Create a GH issue based on the content of the CVE +create_github_issue() { + local title="$1" + local body="$2" + + local api_url="https://api.github.com/repos/$KEYCLOAK_REPO/issues" + local data=$(jq -n --arg title "$title" --arg body "$body" --arg branch "backport/$BRANCH_NAME" \ + '{title: $title, body: $body, labels: ["status/triage", "kind/cve", "kind/bug", $branch]}') + local response=$(curl -s -w "%{http_code}" -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$data" "$api_url") + local http_code=$(echo "$response" | tail -n1) + + if [[ $http_code -eq 201 ]]; then + return 0 + else + printf "Issue creation failed with status: %s\n" "$http_code" + exit 1 + fi +} + +# Update existing issue based on the branches affected +update_github_issue() { + local issue_id="$1" + local api_url="https://api.github.com/repos/$KEYCLOAK_REPO/issues/$issue_id" + local existing_labels=$(curl -s -H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" "$api_url" | jq '.labels | .[].name' | jq -s .) + local new_label="backport/$BRANCH_NAME" + local updated_labels=$(echo "$existing_labels" | jq --arg new_label "$new_label" '. + [$new_label] | unique') + local data=$(jq -n --argjson labels "$updated_labels" '{labels: $labels}') + local response=$(curl -s -w "%{http_code}" -X PATCH -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$data" "$api_url") + local http_code=$(echo "$response" | tail -n1) + + if [[ $http_code -eq 200 ]]; then + return 0 + else + printf "Issue update failed with status: %s\n" "$http_code" + exit 1 + fi +} + +check_dependencies() { + command -v jq >/dev/null 2>&1 || { echo >&2 "jq is required. Exiting."; exit 1; } +} + +# Parse the CVE report coming from SNYK +parse_and_process_vulnerabilities() { + jq -c '.vulnerabilities[] | select(.type != "license")' | while IFS= read -r vulnerability; do + local cve_title=$(echo "$vulnerability" | jq -r '(.identifiers.CVE[0] // .id) + " - " + (.title // "N/A")') + local module=$(echo "$vulnerability" | jq -r '((.mavenModuleName.groupId // "unknown") + ":" + (.mavenModuleName.artifactId // "unknown"))') + local title="${cve_title} in ${module}" + local from_path=$(echo "$vulnerability" | jq -r 'if .from != [] then "Introduced through: " + (.from | join(" › ")) else "" end') + local description=$(echo "$vulnerability" | jq -r '.description // "N/A"') + + printf -v body "%s\n%s\n%s\n%s" "$title" "$module" "$from_path" "$description" + issue_id=$(check_github_issue_exists "$cve_title") + if [[ $issue_id -eq 1 ]]; then + create_github_issue "$title" "$body" + else + update_github_issue "$issue_id" + fi + done +} + +main() { + check_dependencies + + if [ -t 0 ]; then + echo "Error: No input provided. Please pipe in a JSON file." + echo "Usage: cat snyk-report.json | $0" + exit 1 + else + parse_and_process_vulnerabilities + fi +} + +main "$@" diff --git a/.github/workflows/snyk-analysis.yml b/.github/workflows/snyk-analysis.yml index 33dd9cf78963..3f882b7a049c 100644 --- a/.github/workflows/snyk-analysis.yml +++ b/.github/workflows/snyk-analysis.yml @@ -3,6 +3,9 @@ name: Snyk on: workflow_dispatch: +env: + MAVEN_ARGS: "-B -nsu -Daether.connector.http.connectionMaxTtl=25" + defaults: run: shell: bash @@ -13,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: github.repository == 'keycloak/keycloak' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build Keycloak uses: ./.github/actions/build-keycloak @@ -21,27 +24,17 @@ jobs: - uses: snyk/actions/setup@master - name: Check for vulnerabilities in Quarkus - run: snyk test --policy-path=${GITHUB_WORKSPACE}/.github/snyk/.snyk --all-projects --prune-repeated-subdependencies --exclude=tests --sarif-file-output=quarkus-report.sarif quarkus/deployment + run: snyk test --policy-path=${GITHUB_WORKSPACE}/.github/snyk/.snyk --all-projects --prune-repeated-subdependencies --exclude=tests --json quarkus/deployment | .github/scripts/snyk-report.sh continue-on-error: true env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - name: Upload Quarkus scanner results to GitHub - uses: github/codeql-action/upload-sarif@v2.21.5 - with: - sarif_file: quarkus-report.sarif - category: snyk-quarkus-report - - name: Check for vulnerabilities in Operator run: | - mvn -Poperator -pl operator -am -DskipTests clean install - snyk test --policy-path=${GITHUB_WORKSPACE}/.github/snyk/.snyk --all-projects --prune-repeated-subdependencies --exclude=tests --sarif-file-output=operator-report.sarif operator + ./mvnw -Poperator -pl operator -am -DskipTests clean install + snyk test --policy-path=${GITHUB_WORKSPACE}/.github/snyk/.snyk --all-projects --prune-repeated-subdependencies --exclude=tests --json operator | .github/scripts/snyk-report.sh continue-on-error: true env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - - name: Upload Operator scanner results to GitHub - uses: github/codeql-action/upload-sarif@v2.21.5 - with: - sarif_file: operator-report.sarif - category: snyk-operator-report