diff --git a/.github/actions/jira_vuln_k8s/action.yaml b/.github/actions/jira_vuln_k8s/action.yaml new file mode 100644 index 000000000..0b5346343 --- /dev/null +++ b/.github/actions/jira_vuln_k8s/action.yaml @@ -0,0 +1,159 @@ +name: 'Create JIRA Tickets for Docker Scout CVEs' +description: 'Parses Docker Scout SARIF output and creates JIRA tickets for critical/high vulnerabilities' +inputs: + image-tag: + description: 'Docker image tag that was scanned' + required: true + workflow-url: + description: 'GitHub Actions workflow URL' + required: true + commit-sha: + description: 'Git commit SHA' + required: true + jira-base-url: + description: 'JIRA base URL' + required: true + jira-user-email: + description: 'JIRA user email' + required: true + jira-api-token: + description: 'JIRA API token' + required: true + jira-team-field-id: + description: 'JIRA custom field ID for team' + required: false + default: '' +outputs: + vulnerability-count: + description: 'Number of vulnerabilities found' + value: ${{ steps.parse-cves.outputs.count }} + ticket-count: + description: 'Number of JIRA tickets created' + value: ${{ steps.create-tickets.outputs.ticket-count }} +runs: + using: 'composite' + steps: + - name: Parse SARIF for Critical/High CVEs + id: parse-cves + shell: bash + run: | + # Extract CVEs from SARIF + jq -r '.runs[0].results[] | + select(.level == "error" or .level == "warning") | + @json' sarif.output.json > vulnerabilities.json + + # Count vulnerabilities + echo "count=$(jq -s 'length' vulnerabilities.json)" >> $GITHUB_OUTPUT + + - name: Create Jira tickets with actionable details + id: create-tickets + if: steps.parse-cves.outputs.count != '0' + shell: bash + env: + JIRA_BASE_URL: ${{ inputs.jira-base-url }} + JIRA_USER_EMAIL: ${{ inputs.jira-user-email }} + JIRA_API_TOKEN: ${{ inputs.jira-api-token }} + IMAGE_TAG: ${{ inputs.image-tag }} + WORKFLOW_URL: ${{ inputs.workflow-url }} + COMMIT_SHA: ${{ inputs.commit-sha }} + TEAM_ID: ${{ inputs.jira-team-field-id }} + run: | + set +e + + TICKET_COUNT=0 + ERROR_COUNT=0 + CREATED_TICKETS=() + + while IFS= read -r vuln; do + CVE=$(echo "$vuln" | jq -r '.ruleId') + PACKAGE=$(echo "$vuln" | jq -r '.locations[0].physicalLocation.artifactLocation.uri // "unknown"') + VERSION=$(echo "$vuln" | jq -r '.locations[0].physicalLocation.region.snippet.text // "unknown"') + RAW_MESSAGE=$(echo "$vuln" | jq -r '.message.text') + SEVERITY=$(echo "$vuln" | jq -r '.level') + + # Sanitize MESSAGE: remove control characters but keep newlines/tabs + MESSAGE=$(echo "$RAW_MESSAGE" | tr -d '\000-\010\013\014\016-\037') + + SEVERITY_TEXT=$([ "$SEVERITY" = "error" ] && echo "Critical" || echo "High") + REPORT_DATE=$(date +%Y-%m-%d) + VULN_COMPONENT="${PACKAGE}-${VERSION}" + + # Build request from template + JIRA_REQUEST=$(jq \ + --arg cve "$CVE" \ + --arg pkg "$PACKAGE" \ + --arg ver "$VERSION" \ + --arg sev "$SEVERITY_TEXT" \ + --arg img "$IMAGE_TAG" \ + --arg msg "$MESSAGE" \ + --arg url "$WORKFLOW_URL" \ + --arg commit "$COMMIT_SHA" \ + --arg summary "${CVE}: Python - ${PACKAGE}" \ + --arg teamId "$TEAM_ID" \ + --arg reportDate "$REPORT_DATE" \ + --arg cveArray "[${CVE}]" \ + --arg vulnComponent "$VULN_COMPONENT" \ + ' + walk(if type == "string" then + gsub("\\$CVE"; $cve) | + gsub("\\$PACKAGE"; $pkg) | + gsub("\\$VERSION"; $ver) | + gsub("\\$SEVERITY"; $sev) | + gsub("\\$IMAGE"; $img) | + gsub("\\$MESSAGE"; $msg) | + gsub("\\$WORKFLOW_URL"; $url) | + gsub("\\$COMMIT"; $commit) | + gsub("\\$REPORT_DATE"; $reportDate) | + gsub("\\$CVE_ARRAY"; $cveArray) | + gsub("\\$VULN_COMPONENT"; $vulnComponent) + else . end) | + .fields.summary = $summary | + .fields.priority.name = $sev | + if $teamId != "" then + .fields.customfield_10001 = $teamId + else . end + ' .github/templates/jira-security-vuln.json) + + TEMP_REQUEST=$(mktemp) + printf '%s\n' "$JIRA_REQUEST" > "$TEMP_REQUEST" + + JIRA_RESPONSE=$(curl -s --http1.1 -w "\n%{http_code}" --connect-timeout 30 --max-time 90 \ + -X POST "${JIRA_BASE_URL}/rest/api/3/issue" \ + -u "${JIRA_USER_EMAIL}:${JIRA_API_TOKEN}" \ + -H "Content-Type: application/json; charset=utf-8" \ + -H "Accept: application/json" \ + --data-binary @"$TEMP_REQUEST") + + HTTP_CODE=$(echo "$JIRA_RESPONSE" | tail -n1) + RESPONSE_BODY=$(echo "$JIRA_RESPONSE" | sed '$d') + rm -f "$TEMP_REQUEST" + + # Process response + if [[ "$HTTP_CODE" =~ ^2[0-9][0-9]$ ]]; then + JIRA_KEY=$(echo "$RESPONSE_BODY" | jq -r '.key // "null"') + if [ "$JIRA_KEY" != "null" ] && [ -n "$JIRA_KEY" ]; then + CREATED_TICKETS+=("$JIRA_KEY") + ((TICKET_COUNT++)) + else + echo "❌ ${CVE}: Failed to parse ticket ID (HTTP ${HTTP_CODE})" + ((ERROR_COUNT++)) + fi + else + echo "❌ ${CVE}: HTTP ${HTTP_CODE}" + ((ERROR_COUNT++)) + fi + + done < vulnerabilities.json + + echo "=========================================" + if [ $TICKET_COUNT -gt 0 ]; then + TICKET_LIST=$(IFS=', '; echo "${CREATED_TICKETS[*]}") + echo "✅ Created ${TICKET_COUNT} tickets: ${TICKET_LIST}" + fi + + echo "ticket-count=${TICKET_COUNT}" >> $GITHUB_OUTPUT + + if [ $ERROR_COUNT -gt 0 ]; then + echo "❌ Failed to create ${ERROR_COUNT} tickets" + exit 1 + fi diff --git a/.github/templates/jira-security-vuln.json b/.github/templates/jira-security-vuln.json new file mode 100644 index 000000000..bf8292468 --- /dev/null +++ b/.github/templates/jira-security-vuln.json @@ -0,0 +1,104 @@ +{ + "fields": { + "project": { + "key": "NH" + }, + "summary": "$SUMMARY", + "description": { + "version": 1, + "type": "doc", + "content": [ + { + "type": "heading", + "attrs": {"level": 2}, + "content": [{"type": "text", "text": "Security Vulnerability Detected"}] + }, + { + "type": "paragraph", + "content": [ + {"type": "text", "text": "CVE: ", "marks": [{"type": "strong"}]}, + {"type": "text", "text": "$CVE"} + ] + }, + { + "type": "paragraph", + "content": [ + {"type": "text", "text": "Package: ", "marks": [{"type": "strong"}]}, + {"type": "text", "text": "$PACKAGE"} + ] + }, + { + "type": "paragraph", + "content": [ + {"type": "text", "text": "Version: ", "marks": [{"type": "strong"}]}, + {"type": "text", "text": "$VERSION"} + ] + }, + { + "type": "paragraph", + "content": [ + {"type": "text", "text": "Severity: ", "marks": [{"type": "strong"}]}, + {"type": "text", "text": "$SEVERITY"} + ] + }, + { + "type": "paragraph", + "content": [ + {"type": "text", "text": "Image: ", "marks": [{"type": "strong"}]}, + {"type": "text", "text": "$IMAGE"} + ] + }, + { + "type": "heading", + "attrs": {"level": 3}, + "content": [{"type": "text", "text": "Description"}] + }, + { + "type": "paragraph", + "content": [{"type": "text", "text": "$MESSAGE"}] + }, + { + "type": "heading", + "attrs": {"level": 3}, + "content": [{"type": "text", "text": "Recommended Action"}] + }, + { + "type": "paragraph", + "content": [{"type": "text", "text": "Upgrade the vulnerable dependency to the latest secure version."}] + }, + { + "type": "heading", + "attrs": {"level": 3}, + "content": [{"type": "text", "text": "Detection Source"}] + }, + { + "type": "paragraph", + "content": [ + {"type": "text", "text": "Workflow Run: ", "marks": [{"type": "strong"}]}, + {"type": "text", "text": "$WORKFLOW_URL", "marks": [{"type": "link", "attrs": {"href": "$WORKFLOW_URL"}}]} + ] + }, + { + "type": "paragraph", + "content": [ + {"type": "text", "text": "Commit: ", "marks": [{"type": "strong"}]}, + {"type": "text", "text": "$COMMIT"} + ] + } + ] + }, + "issuetype": { + "name": "Security Vuln" + }, + "components": [ + {"name": "Instrument Python"} + ], + "customfield_10369": "$REPORT_DATE", + "customfield_10374": ["$CVE"], + "customfield_10392": "$VULN_COMPONENT", + "labels": ["docker-scout"], + "priority": { + "name": "Critical" + } + } +} diff --git a/.github/workflows/build_publish_image_autoinstrumentation.yaml b/.github/workflows/build_publish_image_autoinstrumentation.yaml index 8fd76375f..cda04b05e 100644 --- a/.github/workflows/build_publish_image_autoinstrumentation.yaml +++ b/.github/workflows/build_publish_image_autoinstrumentation.yaml @@ -97,6 +97,17 @@ jobs: with: sarif_file: sarif.output.json + - name: Create JIRA tickets for vulnerabilities + uses: ./.github/actions/jira_vuln_k8s + with: + image-tag: ${{ steps.meta.outputs.tags[0] }} + workflow-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + commit-sha: ${{ github.sha }} + jira-base-url: ${{ secrets.JIRA_BASE_URL }} + jira-user-email: ${{ secrets.JIRA_USER_EMAIL }} + jira-api-token: ${{ secrets.JIRA_API_TOKEN }} + jira-team-field-id: ${{ secrets.JIRA_TEAM_FIELD_ID }} + ghcr_io: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/build_publish_image_autoinstrumentation_beta.yaml b/.github/workflows/build_publish_image_autoinstrumentation_beta.yaml index 4713433bb..05768935d 100644 --- a/.github/workflows/build_publish_image_autoinstrumentation_beta.yaml +++ b/.github/workflows/build_publish_image_autoinstrumentation_beta.yaml @@ -97,6 +97,17 @@ jobs: with: sarif_file: sarif.output.json + - name: Create JIRA tickets for vulnerabilities + uses: ./.github/actions/jira_vuln_k8s + with: + image-tag: ${{ steps.meta.outputs.tags[0] }} + workflow-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + commit-sha: ${{ github.sha }} + jira-base-url: ${{ secrets.JIRA_BASE_URL }} + jira-user-email: ${{ secrets.JIRA_USER_EMAIL }} + jira-api-token: ${{ secrets.JIRA_API_TOKEN }} + jira-team-field-id: ${{ secrets.JIRA_TEAM_FIELD_ID }} + ghcr_io: runs-on: ubuntu-latest steps: