@@ -25,16 +25,31 @@ jobs:
2525 - name : Install dependencies
2626 run : npm ci
2727
28- - name : Audit dependencies
28+ - name : Run security audit
2929 id : audit
3030 run : |
31- AUDIT_OUTPUT=$(npm audit --audit-level=moderate --json 2>/dev/null || true)
31+ set +e
32+ npm audit --audit-level=moderate --json > audit_results.json 2>audit_error.log
33+ AUDIT_EXIT_CODE=$?
34+
35+ if [ $AUDIT_EXIT_CODE -eq 1 ]; then
36+ echo "Vulnerabilities found (exit code 1)"
37+ echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT
38+ elif [ $AUDIT_EXIT_CODE -ne 0 ]; then
39+ echo "Audit failed with exit code: $AUDIT_EXIT_CODE"
40+ echo '{"error": "audit_failed", "exit_code": '$AUDIT_EXIT_CODE'}' > audit_results.json
41+ echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT
42+ echo "audit_failed=true" >> $GITHUB_OUTPUT
43+ else
44+ echo "No vulnerabilities found"
45+ echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT
46+ fi
47+
48+ AUDIT_OUTPUT=$(cat audit_results.json)
3249 echo "audit_output<<EOF" >> $GITHUB_OUTPUT
3350 echo "$AUDIT_OUTPUT" >> $GITHUB_OUTPUT
3451 echo "EOF" >> $GITHUB_OUTPUT
3552
36- npm audit --audit-level=moderate || echo "::warning::npm audit found vulnerabilities"
37-
3853 - name : Upload audit report
3954 if : always()
4055 uses : actions/upload-artifact@v4
@@ -43,129 +58,215 @@ jobs:
4358 path : |
4459 package-lock.json
4560 package.json
61+ audit_results.json
62+ audit_error.log
4663 retention-days : 7
4764
48- - name : Create issue if vulnerable
49- if : failure()
65+ - name : Process audit results
66+ id : process-results
67+ if : always() && steps.audit.outputs.audit_output
68+ run : |
69+ AUDIT_OUTPUT='${{ steps.audit.outputs.audit_output }}'
70+
71+ # Create a simple Node.js script to process the JSON
72+ cat > process_audit.js << 'EOF'
73+ try {
74+ const auditData = JSON.parse(process.argv[1]);
75+
76+ if (auditData.error) {
77+ console.log('::error::Audit failed: ' + auditData.error);
78+ process.exit(1);
79+ }
80+
81+ const vulnerabilities = auditData.metadata?.vulnerabilities || {};
82+ const total = vulnerabilities.total || 0;
83+ const critical = vulnerabilities.critical || 0;
84+ const high = vulnerabilities.high || 0;
85+ const moderate = vulnerabilities.moderate || 0;
86+ const low = vulnerabilities.low || 0;
87+
88+ console.log(`total=${total}`);
89+ console.log(`critical=${critical}`);
90+ console.log(`high=${high}`);
91+ console.log(`moderate=${moderate}`);
92+ console.log(`low=${low}`);
93+
94+ // Create a summary string
95+ const summary = `Critical: ${critical}, High: ${high}, Moderate: ${moderate}, Low: ${low}, Total: ${total}`;
96+ console.log(`summary=${summary}`);
97+
98+ if (total > 0) {
99+ console.log('::error::Vulnerabilities found: ' + summary);
100+ process.exit(1);
101+ }
102+
103+ } catch (error) {
104+ console.log('::error::Failed to parse audit results: ' + error.message);
105+ process.exit(1);
106+ }
107+ EOF
108+
109+ # Run the processing script and capture outputs
110+ node process_audit.js "$AUDIT_OUTPUT" >> $GITHUB_OUTPUT
111+
112+ - name : Create security issue
113+ if : |
114+ always() &&
115+ steps.audit.outputs.has_vulnerabilities == 'true' &&
116+ github.event_name != 'pull_request'
50117 uses : actions/github-script@v6
51118 with :
52119 github-token : ${{ secrets.GITHUB_TOKEN }}
53120 script : |
121+ const { owner, repo } = context.repo;
122+ const runId = context.runId;
123+ const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${runId}`;
124+
125+ const auditOutput = `${{ steps.audit.outputs.audit_output }}`;
126+ const summary = `${{ steps.process-results.outputs.summary }}`;
127+
128+ let vulnerabilityDetails = 'Unable to parse vulnerability details';
129+ let auditData = {};
130+
54131 try {
55- const auditOutput = `${{ steps.audit.outputs.audit_output }}`;
56- let auditData = {};
57- let vulnerabilitySummary = 'Unable to parse audit details';
58-
59- try {
60- auditData = JSON.parse(auditOutput);
61- if (auditData.metadata && auditData.metadata.vulnerabilities) {
62- const vulns = auditData.metadata.vulnerabilities;
63- vulnerabilitySummary = `Vulnerabilities found:
64- - Critical: ${vulns.critical || 0}
65- - High: ${vulns.high || 0}
66- - Moderate: ${vulns.moderate || 0}
67- - Low: ${vulns.low || 0}`;
68- }
69- } catch (parseError) {
70- console.error('Failed to parse audit output:', parseError.message);
71- vulnerabilitySummary = 'Failed to parse audit output. Check workflow artifacts for details.';
72- }
73-
74- let existingIssues;
75- try {
76- existingIssues = await github.rest.issues.listForRepo({
77- owner: context.repo.owner,
78- repo: context.repo.repo,
79- labels: ['security', 'vulnerability'],
80- state: 'open'
81- });
82- } catch (apiError) {
83- console.error('Failed to fetch existing issues:', apiError.message);
84- existingIssues = { data: [] };
85- }
86-
87- const existingIssue = existingIssues.data.find(issue =>
88- issue.title.includes('Security vulnerabilities detected') ||
89- issue.title.includes('🚨 Security vulnerabilities detected')
90- );
91-
92- if (existingIssue) {
93- console.warn('Security issue already exists: #' + existingIssue.number);
94- return;
95- }
96-
97- try {
98- await github.rest.issues.create({
99- owner: context.repo.owner,
100- repo: context.repo.repo,
101- title: '🚨 Security vulnerabilities detected in dependencies',
102- body: `## Security Audit Report
103-
104- npm audit found vulnerabilities that require attention.
105-
106- **Vulnerability Summary:**
107- \`\`\`
108- ${vulnerabilitySummary}
109- \`\`\`
110-
111- **Next Steps:**
112- 1. Run \`npm audit fix\` to attempt automatic fixes
113- 2. Review the full audit report in the workflow artifacts
114- 3. Update vulnerable dependencies manually if needed
115-
116- **Workflow Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
117-
118- *This issue was automatically generated by the security audit workflow.*`,
119- labels: ['security', 'vulnerability', 'automated']
120- });
121- } catch (createError) {
122- console.error('Failed to create security issue:', createError.message);
132+ auditData = JSON.parse(auditOutput);
133+ if (auditData.metadata && auditData.metadata.vulnerabilities) {
134+ const vulns = auditData.metadata.vulnerabilities;
135+ vulnerabilityDetails = `## Vulnerability Summary
136+
137+ | Severity | Count |
138+ |----------|-------|
139+ | 🔴 Critical | ${vulns.critical || 0} |
140+ | 🟠 High | ${vulns.high || 0} |
141+ | 🟡 Moderate | ${vulns.moderate || 0} |
142+ | 🔵 Low | ${vulns.low || 0} |
143+ | **Total** | **${vulns.total || 0}** |`;
123144 }
145+ } catch (parseError) {
146+ vulnerabilityDetails = `## Audit Results
147+ Unable to parse detailed vulnerability information. Check the workflow artifacts for complete details.`;
148+ }
149+
150+ // Check for existing open security issues
151+ const { data: existingIssues } = await github.rest.issues.listForRepo({
152+ owner,
153+ repo,
154+ labels: ['security-audit', 'dependencies'],
155+ state: 'open'
156+ });
157+
158+ const existingIssue = existingIssues.find(issue =>
159+ issue.title.includes('Security Audit: Vulnerabilities Detected') ||
160+ issue.title.includes('npm audit found vulnerabilities')
161+ );
162+
163+ if (existingIssue) {
164+ console.log(`Security issue already exists: #${existingIssue.number}`);
124165
125- } catch (outerError) {
126- console.error('Unexpected error in Create issue step:', outerError.message);
166+ // Update existing issue with latest results
167+ await github.rest.issues.update({
168+ owner,
169+ repo,
170+ issue_number: existingIssue.number,
171+ body: `## 🔒 Security Audit Update
172+
173+ ${vulnerabilityDetails}
174+
175+ **Latest Scan Results:** ${summary}
176+
177+ **Workflow Run:** [View Run](${runUrl})
178+ **Last Updated:** ${new Date().toISOString()}
179+
180+ **Next Steps:**
181+ 1. Run \`npm audit fix\` to attempt automatic fixes
182+ 2. Run \`npm audit\` to review detailed findings
183+ 3. Manually update dependencies if automatic fixes are insufficient
184+ 4. Check the workflow artifacts for complete audit report
185+
186+ *This issue is automatically updated by the security audit workflow.*`
187+ });
188+ } else {
189+ // Create new issue
190+ await github.rest.issues.create({
191+ owner,
192+ repo,
193+ title: `🔒 Security Audit: Vulnerabilities Detected - ${new Date().toISOString().split('T')[0]}`,
194+ body: `## 🔒 Security Audit Report
195+
196+ ${vulnerabilityDetails}
197+
198+ **Workflow Run:** [View Run](${runUrl})
199+ **Detected:** ${new Date().toISOString()}
200+
201+ **Recommended Actions:**
202+ 1. Run \`npm audit fix\` to attempt automatic fixes
203+ 2. Run \`npm audit\` to review detailed findings
204+ 3. Manually update vulnerable dependencies if needed
205+ 4. Check the workflow artifacts for complete audit report
206+ 5. Close this issue once vulnerabilities are resolved
207+
208+ **Automated Commands:**
209+ \`\`\`bash
210+ # Try automatic fixes
211+ npm audit fix
212+
213+ # Review remaining issues
214+ npm audit
215+ \`\`\`
216+
217+ *This issue was automatically generated by the security audit workflow.*`,
218+ labels: ['security-audit', 'dependencies', 'automated']
219+ });
127220 }
128221
129- - name : Comment on PR if vulnerable
130- if : failure() && github.event_name == 'pull_request'
222+ - name : Comment on PR with vulnerabilities
223+ if : |
224+ always() &&
225+ steps.audit.outputs.has_vulnerabilities == 'true' &&
226+ github.event_name == 'pull_request'
131227 uses : actions/github-script@v6
132228 with :
133229 github-token : ${{ secrets.GITHUB_TOKEN }}
134230 script : |
135- try {
136- const auditOutput = `${{ steps.audit.outputs.audit_output }}`;
137- let vulnerabilitySummary = '## 🔒 Security Audit Results\n\nSecurity vulnerabilities detected by npm audit. Please run `npm audit` to review and fix these issues before merging.';
138-
139- try {
140- const auditData = JSON.parse(auditOutput);
141- if (auditData.metadata && auditData.metadata.vulnerabilities) {
142- const vulns = auditData.metadata.vulnerabilities;
143- vulnerabilitySummary = `## 🔒 Security Audit Results
144-
145- Vulnerabilities found in this PR:
146- - ⚠️ Critical: ${vulns.critical || 0}
147- - 🔴 High: ${vulns.high || 0}
148- - 🟡 Moderate: ${vulns.moderate || 0}
149- - 🔵 Low: ${vulns.low || 0}
150-
151- Please run \`npm audit\` to review and fix these issues before merging.`;
152- }
153- } catch (parseError) {
154- console.error('Failed to parse audit output for PR comment:', parseError.message);
155- vulnerabilitySummary = '## 🔒 Security Audit Results\n\nSecurity vulnerabilities detected. Please check the workflow artifacts for detailed audit results.';
156- }
157-
158- try {
159- await github.rest.issues.createComment({
160- owner: context.repo.owner,
161- repo: context.repo.repo,
162- issue_number: context.payload.pull_request.number,
163- body: vulnerabilitySummary
164- });
165- } catch (commentError) {
166- console.error('Failed to create PR comment:', commentError.message);
167- }
168-
169- } catch (outerError) {
170- console.error('Unexpected error in PR comment step:', outerError.message);
231+ const summary = `${{ steps.process-results.outputs.summary }}`;
232+ const total = `${{ steps.process-results.outputs.total }}`;
233+ const critical = `${{ steps.process-results.outputs.critical }}`;
234+ const high = `${{ steps.process-results.outputs.high }}`;
235+
236+ let severityIcon = '⚠️';
237+ if (critical > 0 || high > 0) {
238+ severityIcon = '🚨';
171239 }
240+
241+ const commentBody = `## ${severityIcon} Security Audit Results
242+
243+ **Vulnerabilities detected in this PR:**
244+ ${summary}
245+
246+ **Required Action:**
247+ Please address these security vulnerabilities before merging this pull request.
248+
249+ **Quick Fixes:**
250+ \`\`\`bash
251+ # Try automatic fixes
252+ npm audit fix
253+
254+ # If that doesn't resolve all issues, try with force
255+ npm audit fix --force
256+ \`\`\`
257+
258+ *Note: Be cautious with \`--force\` as it may introduce breaking changes.*`;
259+
260+ await github.rest.issues.createComment({
261+ owner: context.repo.owner,
262+ repo: context.repo.repo,
263+ issue_number: context.payload.pull_request.number,
264+ body: commentBody
265+ });
266+
267+ - name : Fail workflow if vulnerabilities found
268+ if : steps.audit.outputs.has_vulnerabilities == 'true'
269+ run : |
270+ echo "::error::Security vulnerabilities detected! Check the created issue or PR comment for details."
271+ echo "Summary: ${{ steps.process-results.outputs.summary }}"
272+ exit 1
0 commit comments