MCP Scan Report #541
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # This workflow runs in the context of the base repository and has write access | |
| # to post PR comments, even for PRs from forks. It triggers after the main | |
| # build-containers workflow completes and downloads scan result artifacts. | |
| name: MCP Scan Report | |
| on: | |
| workflow_run: | |
| workflows: ["Build MCP Server Containers"] | |
| types: [completed] | |
| permissions: {} | |
| jobs: | |
| mcp-scan-report: | |
| runs-on: ubuntu-latest | |
| if: github.event.workflow_run.event == 'pull_request' | |
| steps: | |
| - name: Generate GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} | |
| permission-actions: read | |
| permission-pull-requests: write | |
| - name: Download PR number artifact | |
| id: pr-number | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: pr-number | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| - name: Read PR number | |
| id: read-pr | |
| run: | | |
| PR_NUMBER=$(cat pr-number.txt) | |
| echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT | |
| echo "PR number: $PR_NUMBER" | |
| - name: Download scan results | |
| id: scan-results | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: mcp-scan-* | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| path: scan-artifacts | |
| continue-on-error: true | |
| - name: Comment PR with scan results | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| github-token: ${{ steps.app-token.outputs.token }} | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const prNumber = parseInt('${{ steps.read-pr.outputs.pr_number }}'); | |
| console.log('Commenting on PR:', prNumber); | |
| let comment = '## 🔒 MCP Security Scan Results\n\n'; | |
| let hasAnyIssues = false; | |
| let totalServersScanned = 0; | |
| let totalVulnerabilities = 0; | |
| // Find all scan summary files in the scan-artifacts directory | |
| const summaryFiles = []; | |
| const artifactsDir = 'scan-artifacts'; | |
| if (fs.existsSync(artifactsDir)) { | |
| const artifactDirs = fs.readdirSync(artifactsDir); | |
| console.log('Artifact contents:', artifactDirs); | |
| for (const item of artifactDirs) { | |
| const itemPath = path.join(artifactsDir, item); | |
| const stat = fs.statSync(itemPath); | |
| if (stat.isDirectory()) { | |
| const summaryFile = path.join(itemPath, 'scan-summary.json'); | |
| if (fs.existsSync(summaryFile)) { | |
| summaryFiles.push(summaryFile); | |
| console.log('Found summary file in directory:', summaryFile); | |
| } | |
| } else if (stat.isFile() && item === 'scan-summary.json') { | |
| summaryFiles.push(itemPath); | |
| console.log('Found summary file directly:', itemPath); | |
| } | |
| } | |
| } | |
| console.log('Total summary files found:', summaryFiles.length); | |
| if (summaryFiles.length === 0) { | |
| comment += '⚠️ No MCP servers were scanned in this PR.\n'; | |
| } else { | |
| for (const file of summaryFiles) { | |
| try { | |
| const summary = JSON.parse(fs.readFileSync(file, 'utf8')); | |
| totalServersScanned++; | |
| if (summary.status === 'passed') { | |
| comment += `### ✅ ${summary.server}\n`; | |
| comment += `- **Status**: Passed\n`; | |
| comment += `- **Tools scanned**: ${summary.tools_scanned || 0}\n`; | |
| comment += `- **Result**: No security issues detected\n\n`; | |
| } else if (summary.status === 'failed') { | |
| hasAnyIssues = true; | |
| totalVulnerabilities += summary.blocking_count || 0; | |
| comment += `### ❌ ${summary.server}\n`; | |
| comment += `- **Status**: Failed\n`; | |
| comment += `- **Tools scanned**: ${summary.tools_scanned || 0}\n`; | |
| comment += `- **Vulnerabilities found**: ${summary.blocking_count || 0}\n`; | |
| comment += '\n**Security issues detected:**\n'; | |
| if (summary.blocking_issues) { | |
| summary.blocking_issues.forEach(vuln => { | |
| comment += `- **[${vuln.code}]** ${vuln.message}\n`; | |
| }); | |
| } | |
| if (summary.allowed_issues && summary.allowed_issues.length > 0) { | |
| comment += '\n**Allowed issues (not blocking):**\n'; | |
| summary.allowed_issues.forEach(vuln => { | |
| comment += `- **[${vuln.code}]** ${vuln.message} _(Allowed: ${vuln.allowed_reason})_\n`; | |
| }); | |
| } | |
| comment += '\n'; | |
| } else if (summary.status === 'warning') { | |
| comment += `### ⚠️ ${summary.server}\n`; | |
| comment += `- **Status**: Warning\n`; | |
| comment += `- **Message**: ${summary.message}\n\n`; | |
| } else { | |
| comment += `### ⚠️ ${summary.server}\n`; | |
| comment += `- **Status**: Error\n`; | |
| comment += `- **Message**: ${summary.message || 'Unknown error'}\n\n`; | |
| } | |
| } catch (error) { | |
| console.error(`Error parsing ${file}:`, error); | |
| comment += `### ⚠️ Error parsing scan results\n`; | |
| comment += `Could not parse ${file}: ${error.message}\n\n`; | |
| } | |
| } | |
| if (totalServersScanned > 0) { | |
| comment += '---\n'; | |
| comment += `**Summary**: Scanned ${totalServersScanned} MCP server(s)`; | |
| if (hasAnyIssues) { | |
| comment += `, found ${totalVulnerabilities} security issue(s).\n\n`; | |
| comment += '⚠️ **Action Required**: Security issues were detected. Please review and address them before merging.\n'; | |
| } else { | |
| comment += ', all passed security checks. ✅\n'; | |
| } | |
| } | |
| } | |
| // Find and update or create comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const botComment = comments.find(c => | |
| c.user.type === 'Bot' && c.body.includes('MCP Security Scan Results') | |
| ); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: comment | |
| }); | |
| console.log(`Updated existing comment #${botComment.id}`); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: comment | |
| }); | |
| console.log('Created new comment'); | |
| } |