-
Notifications
You must be signed in to change notification settings - Fork 6
172 lines (156 loc) · 7.29 KB
/
Copy pathskill-scan-report.yml
File metadata and controls
172 lines (156 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# 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');
}