mirror of
https://github.com/ivuorinen/actions.git
synced 2026-02-05 19:43:41 +00:00
fix(ci): combine workflows to security-suite Remove legacy GitHub Actions workflows and add security-suite workflow (#58)
* fix(ci): combine workflows to security-suite * fix(ci): tweak permissions
This commit is contained in:
320
.github/workflows/security-suite.yml
vendored
Normal file
320
.github/workflows/security-suite.yml
vendored
Normal file
@@ -0,0 +1,320 @@
|
||||
---
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||
name: Security Suite
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '55 23 * * 0' # Every Sunday at 23:55
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/package.json'
|
||||
- '**/package-lock.json'
|
||||
- '**/yarn.lock'
|
||||
- '**/pnpm-lock.yaml'
|
||||
- '**/requirements.txt'
|
||||
- '**/Dockerfile'
|
||||
- '**/*.py'
|
||||
- '**/*.js'
|
||||
- '**/*.ts'
|
||||
- '**/workflows/*.yml'
|
||||
merge_group:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
jobs:
|
||||
security-checks:
|
||||
name: Security Checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Check Required Secrets
|
||||
id: check-secrets
|
||||
shell: bash
|
||||
run: |
|
||||
{
|
||||
echo "run_snyk=false"
|
||||
echo "run_slack=false"
|
||||
echo "run_sonarcloud=false"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Check secrets
|
||||
if [ -n "${{ secrets.SNYK_TOKEN }}" ]; then
|
||||
echo "run_snyk=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::SNYK_TOKEN not set - Snyk scans will be skipped"
|
||||
fi
|
||||
|
||||
if [ -n "${{ secrets.SLACK_WEBHOOK }}" ]; then
|
||||
echo "run_slack=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::SLACK_WEBHOOK not set - Slack notifications will be skipped"
|
||||
fi
|
||||
|
||||
if [ -n "${{ secrets.SONAR_TOKEN }}" ]; then
|
||||
echo "run_sonarcloud=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::warning::SONAR_TOKEN not set - SonarCloud analysis will be skipped"
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# OWASP Dependency Check
|
||||
- name: Run OWASP Dependency Check
|
||||
uses: dependency-check/Dependency-Check_Action@3102a65fd5f36d0000297576acc56a475b0de98d # main
|
||||
with:
|
||||
project: 'GitHub Actions'
|
||||
path: '.'
|
||||
format: 'SARIF'
|
||||
out: 'reports'
|
||||
args: >
|
||||
--enableRetired
|
||||
--enableExperimental
|
||||
--failOnCVSS 7
|
||||
|
||||
- name: Upload OWASP Results
|
||||
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
with:
|
||||
sarif_file: reports/dependency-check-report.sarif
|
||||
category: owasp-dependency-check
|
||||
|
||||
# Snyk Analysis
|
||||
- name: Setup Node.js
|
||||
if: steps.check-secrets.outputs.run_snyk == 'true'
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Run Snyk Scan
|
||||
id: snyk
|
||||
if: steps.check-secrets.outputs.run_snyk == 'true'
|
||||
uses: snyk/actions/node@cdb760004ba9ea4d525f2e043745dfe85bb9077e # master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --all-projects --sarif-file-output=snyk-results.sarif
|
||||
|
||||
- name: Upload Snyk Results
|
||||
if: steps.check-secrets.outputs.run_snyk == 'true'
|
||||
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
with:
|
||||
sarif_file: snyk-results.sarif
|
||||
category: snyk
|
||||
|
||||
# OSSF Scorecard
|
||||
- name: Run Scorecard
|
||||
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
|
||||
with:
|
||||
results_file: scorecard-results.sarif
|
||||
results_format: sarif
|
||||
publish_results: true
|
||||
|
||||
- name: Upload Scorecard Results
|
||||
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
||||
with:
|
||||
sarif_file: scorecard-results.sarif
|
||||
category: scorecard
|
||||
|
||||
# Analysis and Metrics
|
||||
- name: Analyze Results
|
||||
id: analysis
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
async function analyzeResults() {
|
||||
const metrics = {
|
||||
timestamp: new Date().toISOString(),
|
||||
vulnerabilities: { critical: 0, high: 0, medium: 0, low: 0 },
|
||||
scorecard: null,
|
||||
trends: {},
|
||||
tools: {}
|
||||
};
|
||||
|
||||
function analyzeSarif(file, tool) {
|
||||
if (!fs.existsSync(file)) return null;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
const results = {
|
||||
total: 0,
|
||||
bySeverity: { critical: 0, high: 0, medium: 0, low: 0 },
|
||||
details: []
|
||||
};
|
||||
|
||||
data.runs.forEach(run => {
|
||||
if (!run.results) return;
|
||||
|
||||
run.results.forEach(result => {
|
||||
results.total++;
|
||||
const severity = result.level === 'error' ? 'high' :
|
||||
result.level === 'warning' ? 'medium' : 'low';
|
||||
|
||||
results.bySeverity[severity]++;
|
||||
metrics.vulnerabilities[severity]++;
|
||||
|
||||
results.details.push({
|
||||
title: result.message?.text || 'Unnamed issue',
|
||||
severity,
|
||||
location: result.locations?.[0]?.physicalLocation?.artifactLocation?.uri || 'Unknown',
|
||||
description: result.message?.text || '',
|
||||
ruleId: result.ruleId || ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(`Error analyzing ${tool} results:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze all SARIF files
|
||||
metrics.tools = {
|
||||
owasp: analyzeSarif('reports/dependency-check-report.sarif', 'OWASP'),
|
||||
snyk: ${{ steps.check-secrets.outputs.run_snyk == 'true' }} ?
|
||||
analyzeSarif('snyk-results.sarif', 'Snyk') : null,
|
||||
scorecard: analyzeSarif('scorecard-results.sarif', 'Scorecard')
|
||||
};
|
||||
|
||||
// Save results for other steps
|
||||
fs.writeFileSync('security-results.json', JSON.stringify(metrics, null, 2));
|
||||
|
||||
// Set outputs for other steps
|
||||
core.setOutput('total_critical', metrics.vulnerabilities.critical);
|
||||
core.setOutput('total_high', metrics.vulnerabilities.high);
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
return await analyzeResults();
|
||||
|
||||
- name: Generate Reports
|
||||
if: always()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const metrics = JSON.parse(fs.readFileSync('security-results.json', 'utf8'));
|
||||
|
||||
// Find existing security report issue
|
||||
const issues = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
labels: ['security-report'],
|
||||
per_page: 1
|
||||
});
|
||||
|
||||
const severityEmoji = {
|
||||
critical: '🚨',
|
||||
high: '⚠️',
|
||||
medium: '⚡',
|
||||
low: '📝'
|
||||
};
|
||||
|
||||
// Generate report body
|
||||
const report = `## Security Scan Report ${new Date().toISOString()}
|
||||
|
||||
### Summary
|
||||
${Object.entries(metrics.vulnerabilities)
|
||||
.map(([sev, count]) => `${severityEmoji[sev]} ${sev}: ${count}`)
|
||||
.join('\n')}
|
||||
|
||||
### Tool Results
|
||||
${Object.entries(metrics.tools)
|
||||
.filter(([_, results]) => results)
|
||||
.map(([tool, results]) => `
|
||||
#### ${tool.toUpperCase()}
|
||||
- Total issues: ${results.total}
|
||||
${Object.entries(results.bySeverity)
|
||||
.filter(([_, count]) => count > 0)
|
||||
.map(([sev, count]) => `- ${sev}: ${count}`)
|
||||
.join('\n')}
|
||||
|
||||
${results.details
|
||||
.filter(issue => ['critical', 'high'].includes(issue.severity))
|
||||
.map(issue => `- ${severityEmoji[issue.severity]} ${issue.title} (${issue.severity})
|
||||
- Location: \`${issue.location}\`
|
||||
- Rule: \`${issue.ruleId}\``)
|
||||
.join('\n')}
|
||||
`).join('\n')}
|
||||
|
||||
### Action Items
|
||||
${metrics.vulnerabilities.critical + metrics.vulnerabilities.high > 0 ?
|
||||
`- [ ] Address ${metrics.vulnerabilities.critical} critical and ${metrics.vulnerabilities.high} high severity issues
|
||||
- [ ] Review automated fix PRs
|
||||
- [ ] Update dependencies with known vulnerabilities` :
|
||||
'✅ No critical or high severity issues found'}
|
||||
|
||||
### Links
|
||||
- [Workflow Run](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
|
||||
- [Security Overview](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/security)
|
||||
|
||||
> Last updated: ${new Date().toISOString()}`;
|
||||
|
||||
// Update or create issue
|
||||
if (issues.data.length > 0) {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issues.data[0].number,
|
||||
body: report,
|
||||
state: 'open'
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: '🔒 Security Scan Report',
|
||||
body: report,
|
||||
labels: ['security-report', 'automated'],
|
||||
assignees: ['ivuorinen']
|
||||
});
|
||||
}
|
||||
|
||||
// Add summary to workflow
|
||||
await core.summary
|
||||
.addRaw(report)
|
||||
.write();
|
||||
|
||||
- name: Archive Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: security-results
|
||||
path: |
|
||||
reports/
|
||||
*.sarif
|
||||
security-results.json
|
||||
retention-days: 30
|
||||
|
||||
- name: Notify on Failure
|
||||
if: failure() && steps.check-secrets.outputs.run_slack == 'true'
|
||||
run: |
|
||||
curl -X POST -H 'Content-type: application/json' \
|
||||
--data '{"text":"❌ Security checks failed! Check the logs for details."}' \
|
||||
${{ secrets.SLACK_WEBHOOK }}
|
||||
Reference in New Issue
Block a user