--- # 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"