Skip to content

MCP Scan Report

MCP Scan Report #541

# 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');
}