Skill Scan Report #224
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
| # Runs in the base repository context so it can comment on PRs from forks. | |
| # Triggered after Build Skill Artifacts completes; downloads scan artifacts | |
| # and upserts a single "Skill Security Scan Results" PR comment. | |
| name: Skill Scan Report | |
| on: | |
| workflow_run: | |
| workflows: ["Build Skill Artifacts"] | |
| types: [completed] | |
| permissions: | |
| pull-requests: write | |
| jobs: | |
| skill-scan-report: | |
| runs-on: ubuntu-latest | |
| if: github.event.workflow_run.event == 'pull_request' | |
| steps: | |
| - name: Download PR number artifact | |
| id: pr-number | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: skill-pr-number | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_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 | |
| with: | |
| pattern: skill-scan-* | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| path: scan-artifacts | |
| continue-on-error: true | |
| - name: Comment PR with scan results | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 | |
| with: | |
| 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 = '## 🛡️ Skill Security Scan Results\n\n'; | |
| let hasAnyIssues = false; | |
| let totalSkillsScanned = 0; | |
| let totalBlocking = 0; | |
| const summaryFiles = []; | |
| const artifactsDir = 'scan-artifacts'; | |
| if (fs.existsSync(artifactsDir)) { | |
| for (const item of fs.readdirSync(artifactsDir)) { | |
| 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); | |
| } else if (stat.isFile() && item === 'scan-summary.json') { | |
| summaryFiles.push(itemPath); | |
| } | |
| } | |
| } | |
| if (summaryFiles.length === 0) { | |
| comment += '⚠️ No skills were scanned in this PR.\n'; | |
| } else { | |
| for (const file of summaryFiles) { | |
| try { | |
| const summary = JSON.parse(fs.readFileSync(file, 'utf8')); | |
| totalSkillsScanned++; | |
| const name = summary.skill || 'unknown'; | |
| if (summary.status === 'passed') { | |
| comment += `### ✅ ${name}\n`; | |
| comment += `- **Status**: Passed\n`; | |
| comment += `- **Findings**: ${summary.findings_count || 0}\n`; | |
| if (summary.allowed_issues && summary.allowed_issues.length > 0) { | |
| comment += `- **Allowed (not blocking)**: ${summary.allowed_count}\n`; | |
| for (const issue of summary.allowed_issues) { | |
| comment += ` - \`${issue.code}\` _(Allowed: ${issue.allowed_reason})_\n`; | |
| } | |
| } | |
| comment += '\n'; | |
| } else if (summary.status === 'failed') { | |
| hasAnyIssues = true; | |
| totalBlocking += summary.blocking_count || 0; | |
| comment += `### ❌ ${name}\n`; | |
| comment += `- **Status**: Failed\n`; | |
| comment += `- **Findings**: ${summary.findings_count || 0}\n`; | |
| comment += `- **Blocking**: ${summary.blocking_count || 0}\n\n`; | |
| comment += '**Blocking issues:**\n'; | |
| if (summary.blocking_issues) { | |
| for (const issue of summary.blocking_issues) { | |
| const loc = issue.file_path | |
| ? ` _(${issue.file_path}${issue.line_number ? ':' + issue.line_number : ''})_` | |
| : ''; | |
| comment += `- **[${issue.code}]** (${issue.severity}) ${issue.message}${loc}\n`; | |
| } | |
| } | |
| if (summary.allowed_issues && summary.allowed_issues.length > 0) { | |
| comment += '\n**Allowlisted (not blocking):**\n'; | |
| for (const issue of summary.allowed_issues) { | |
| comment += `- \`${issue.code}\` _(Allowed: ${issue.allowed_reason})_\n`; | |
| } | |
| } | |
| comment += '\n'; | |
| } else if (summary.status === 'warning') { | |
| comment += `### ⚠️ ${name}\n`; | |
| comment += `- **Status**: Warning\n`; | |
| comment += `- **Message**: ${summary.message}\n\n`; | |
| } else { | |
| comment += `### ⚠️ ${name}\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 (totalSkillsScanned > 0) { | |
| comment += '---\n'; | |
| comment += `**Summary**: Scanned ${totalSkillsScanned} skill(s)`; | |
| if (hasAnyIssues) { | |
| comment += `, found ${totalBlocking} blocking issue(s).\n\n`; | |
| comment += '⚠️ **Action Required**: Review the blocking findings. Add a justified entry to the skill\'s `security.allowed_issues[]` in its `spec.yaml` if the finding is a false positive.\n'; | |
| } else { | |
| comment += ', all passed security checks. ✅\n'; | |
| } | |
| } | |
| } | |
| 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('Skill 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'); | |
| } |