mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-31 16:41:09 +00:00
230 lines
9.1 KiB
YAML
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"
|