Files
actions/.github/workflows/security-trends.yml
2025-02-04 12:16:16 +02:00

230 lines
9.1 KiB
YAML

---
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Security Trends Analysis
on:
workflow_run:
workflows: ['Security Checks']
types:
- completed
permissions:
contents: read
actions: read
pull-requests: read
jobs:
analyze-trends:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
fetch-depth: 0
- name: Download latest results
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: security-reports-${{ github.event.workflow_run.id }}
path: latest-results
- name: Analyze Trends
id: analyze
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const fs = require('fs');
const path = require('path');
const core = require('@actions/core');
async function collectMetrics() {
const metrics = {
timestamp: new Date().toISOString(),
weekly: {
scans: 0,
vulnerabilities: {
critical: 0,
high: 0,
medium: 0,
low: 0
},
fixes: {
submitted: 0,
merged: 0
},
meanTimeToFix: null // Initialize as null instead of 0
},
history: [],
trends: {
vulnerabilities: {
trend: 'stable',
percentage: 0
},
fixRate: {
trend: 'stable',
percentage: 0
}
}
};
try {
// Check if the artifacts exist
const files = fs.readdirSync('latest-results');
const requiredFiles = ['vulnerability-summary.json', 'security-badge.json', 'security-report.md'];
const missingFiles = requiredFiles.filter(file => !files.includes(file));
if (missingFiles.length > 0) {
throw new Error(`Missing required files: ${missingFiles.join(', ')}`);
}
// Read the latest results
const vulnSummary = JSON.parse(fs.readFileSync('latest-results/vulnerability-summary.json', 'utf8'));
const securityBadge = JSON.parse(fs.readFileSync('latest-results/security-badge.json', 'utf8'));
const securityReport = fs.readFileSync('latest-results/security-report.md', 'utf8');
// Update metrics
metrics.weekly.vulnerabilities = vulnSummary.total;
// Fetch history data if it exists
try {
const historyFile = 'security-metrics-history.json';
if (fs.existsSync(historyFile)) {
const history = JSON.parse(fs.readFileSync(historyFile, 'utf8'));
metrics.history = history.slice(-12); // Säilytetään 12 viikon historia
}
} catch (error) {
console.log('No existing history found, starting fresh');
}
// Collect PR metrics with security-fix label
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
labels: ['security-fix'],
since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
});
// Compute metrics
const securityFixes = prs.data.filter(pr => pr.labels.some(label => label.name === 'security-fix'));
metrics.weekly.fixes = {
submitted: securityFixes.length,
merged: securityFixes.filter(pr => pr.merged_at !== null).length,
automated: securityFixes.filter(pr => pr.labels.some(label => label.name === 'automated-pr')).length
};
// Calculate trends
if (metrics.history.length > 0) {
const lastWeek = metrics.history[metrics.history.length - 1];
const totalVulns = Object.values(metrics.weekly.vulnerabilities).reduce((a, b) => a + b, 0);
const lastWeekVulns = Object.values(lastWeek.weekly.vulnerabilities).reduce((a, b) => a + b, 0);
metrics.trends.vulnerabilities = {
trend: totalVulns < lastWeekVulns ? 'improving' : totalVulns > lastWeekVulns ? 'worsening' : 'stable',
percentage: lastWeekVulns ? ((totalVulns - lastWeekVulns) / lastWeekVulns * 100).toFixed(1) : 0
};
}
// Update history
metrics.history.push({
timestamp: metrics.timestamp,
weekly: metrics.weekly
});
// Save metrics and history
fs.writeFileSync('security-metrics-history.json', JSON.stringify(metrics.history, null, 2));
fs.writeFileSync('security-metrics.json', JSON.stringify(metrics, null, 2));
// Create the report
const report = generateEnhancedReport(metrics, securityReport);
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: '📊 Weekly Security Metrics Report',
body: report,
labels: ['metrics', 'security']
});
} catch (error) {
core.setFailed(`Failed to process security artifacts: ${error.message}`);
throw error;
}
}
function generateEnhancedReport(metrics, securityReport) {
const formatTrend = (trend) => {
const icons = {
improving: '📈',
worsening: '📉',
stable: '➡️'
};
return icons[trend] || '➡️';
};
const formatDuration = (hours) => {
if (hours === null) return 'N/A';
return `${hours} hours`;
};
// Luodaan trendikuvaaja ASCII-grafiikkana
const generateTrendGraph = (history, metric) => {
const values = history.map(h => Object.values(h.weekly[metric]).reduce((a, b) => a + b, 0));
const max = Math.max(...values);
const min = Math.min(...values);
const range = max - min;
const height = 5;
return values.map(v => {
const normalized = range ? Math.floor((v - min) / range * height) : 0;
return '█'.repeat(normalized) + '░'.repeat(height - normalized);
}).join(' ');
};
return `## 📊 Weekly Security Metrics Report
### Timeline
- Report Generated: ${new Date().toISOString()}
- Period: Last 7 days
### Vulnerability Trends ${formatTrend(metrics.trends.vulnerabilities.trend)}
\`\`\`
${generateTrendGraph(metrics.history, 'vulnerabilities')}
\`\`\`
${metrics.trends.vulnerabilities.trend !== 'stable' ?
`Change: ${metrics.trends.vulnerabilities.percentage}% from last week` :
'No significant change from last week'}
### Current Status
- Critical: ${metrics.weekly.vulnerabilities.critical}
- High: ${metrics.weekly.vulnerabilities.high}
- Medium: ${metrics.weekly.vulnerabilities.medium}
- Low: ${metrics.weekly.vulnerabilities.low}
### Fix Metrics
- Security PRs Submitted: ${metrics.weekly.fixes.submitted}
- Automated Fixes: ${metrics.weekly.fixes.automated}
- Successfully Merged: ${metrics.weekly.fixes.merged}
- Mean Time to Fix: ${formatDuration(metrics.weekly.meanTimeToFix)}
### Detailed Reports
- [Full Security Report](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
- [Latest Scan Results](${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/security/advisories)
> This report was automatically generated by the security metrics workflow.`;
}
collectMetrics();
- name: Cleanup
if: always()
shell: bash
run: |
# Remove temporary files but keep the history
rm -f trend-report.md security-trends.svg
echo "Cleaned up temporary files"