fix: security-trends fixes, docs, tweaks

This commit is contained in:
Ismo Vuorinen
2025-02-04 12:16:16 +02:00
parent a23c43af8d
commit a006d699d3
11 changed files with 236 additions and 94 deletions

View File

@@ -78,7 +78,7 @@ jobs:
uses: aquasecurity/trivy-action@a11da62073708815958ea6d84f5650c78a3ef85b # master uses: aquasecurity/trivy-action@a11da62073708815958ea6d84f5650c78a3ef85b # master
with: with:
scan-type: 'fs' scan-type: 'fs'
security-checks: 'vuln,config,secret' scanners: 'vuln,config,secret'
format: 'sarif' format: 'sarif'
output: 'trivy-results.sarif' output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH' severity: 'CRITICAL,HIGH'

View File

@@ -1,3 +1,5 @@
---
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: 'Dependency Review' name: 'Dependency Review'
on: [pull_request] on: [pull_request]

View File

@@ -64,7 +64,7 @@ jobs:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} token: ${{ secrets.FIXIMUS_TOKEN || secrets.GITHUB_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- name: MegaLinter - name: MegaLinter

View File

@@ -1,3 +1,5 @@
---
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Release name: Release
on: on:

View File

@@ -1,7 +1,9 @@
---
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: Scorecard name: Scorecard
on: on:
schedule: schedule:
- cron: '0 2 * * 0' - cron: '0 2 * * 0' # Run every Sunday at 2:00 AM
push: push:
branches: [main] branches: [main]

View File

@@ -8,7 +8,7 @@ on:
types: types:
- completed - completed
schedule: schedule:
- cron: '0 0 * * 0' # Weekly - cron: '0 0 * * 0' # Every Sunday at 00:00
permissions: permissions:
contents: read contents: read

View File

@@ -11,6 +11,7 @@ on:
permissions: permissions:
contents: read contents: read
actions: read actions: read
pull-requests: read
jobs: jobs:
analyze-trends: analyze-trends:
@@ -38,97 +39,187 @@ jobs:
script: | script: |
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const core = require('@actions/core');
try { async function collectMetrics() {
// ... [previous code remains the same until report generation] 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
}
}
};
// Generate trend report try {
const report = generateTrendReport(trends); // 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(', ')}`);
}
// Save report explicitly for next step // Read the latest results
console.log('Writing trend report to file...'); const vulnSummary = JSON.parse(fs.readFileSync('latest-results/vulnerability-summary.json', 'utf8'));
fs.writeFileSync('trend-report.md', report); const securityBadge = JSON.parse(fs.readFileSync('latest-results/security-badge.json', 'utf8'));
console.log('Trend report saved successfully'); const securityReport = fs.readFileSync('latest-results/security-report.md', 'utf8');
// Update metrics
metrics.weekly.vulnerabilities = vulnSummary.total;
// Save history // Fetch history data if it exists
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2)); 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');
}
// Generate and save chart // Collect PR metrics with security-fix label
const chartData = generateChartData(history); const prs = await github.rest.pulls.list({
fs.writeFileSync('security-trends.svg', chartData); owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
labels: ['security-fix'],
since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
});
// Set outputs for other steps // Compute metrics
core.setOutput('has_vulnerabilities', const securityFixes = prs.data.filter(pr => pr.labels.some(label => label.name === 'security-fix'));
trends.critical.current > 0 || trends.high.current > 0); metrics.weekly.fixes = {
core.setOutput('trend_status', submitted: securityFixes.length,
trends.critical.trend > 0 || trends.high.trend > 0 ? 'worsening' : 'improving'); merged: securityFixes.filter(pr => pr.merged_at !== null).length,
core.setOutput('report_path', 'trend-report.md'); automated: securityFixes.filter(pr => pr.labels.some(label => label.name === 'automated-pr')).length
};
} catch (error) { // Calculate trends
core.setFailed(`Failed to analyze trends: ${error.message}`); if (metrics.history.length > 0) {
throw error; 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;
}
} }
- name: Verify Report File function generateEnhancedReport(metrics, securityReport) {
id: verify const formatTrend = (trend) => {
shell: bash const icons = {
run: | improving: '📈',
if [ ! -f "trend-report.md" ]; then worsening: '📉',
echo "::error::Trend report file not found" stable: '➡️'
echo "exists=false" >> "$GITHUB_OUTPUT" };
exit 1 return icons[trend] || '➡️';
else };
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "size=$(stat -f%z trend-report.md)" >> "$GITHUB_OUTPUT"
fi
- name: Create Trend Report Issue const formatDuration = (hours) => {
if: | if (hours === null) return 'N/A';
github.event.workflow_run.conclusion == 'success' && return `${hours} hours`;
steps.verify.outputs.exists == 'true' };
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
try {
const fs = require('fs');
const reportPath = 'trend-report.md';
console.log('Reading trend report from:', reportPath); // Luodaan trendikuvaaja ASCII-grafiikkana
console.log('File size:', '${{ steps.verify.outputs.size }}', 'bytes'); 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(' ');
};
if (!fs.existsSync(reportPath)) { return `## 📊 Weekly Security Metrics Report
throw new Error('Trend report file not found despite verification');
}
const report = fs.readFileSync(reportPath, 'utf8'); ### Timeline
if (!report.trim()) { - Report Generated: ${new Date().toISOString()}
throw new Error('Trend report file is empty'); - Period: Last 7 days
}
const hasVulnerabilities = '${{ steps.analyze.outputs.has_vulnerabilities }}' === 'true'; ### Vulnerability Trends ${formatTrend(metrics.trends.vulnerabilities.trend)}
const trendStatus = '${{ steps.analyze.outputs.trend_status }}'; \`\`\`
${generateTrendGraph(metrics.history, 'vulnerabilities')}
\`\`\`
${metrics.trends.vulnerabilities.trend !== 'stable' ?
`Change: ${metrics.trends.vulnerabilities.percentage}% from last week` :
'No significant change from last week'}
const title = `📊 Security Trend Report - ${ ### Current Status
hasVulnerabilities ? - Critical: ${metrics.weekly.vulnerabilities.critical}
`⚠️ Vulnerabilities ${trendStatus}` : - High: ${metrics.weekly.vulnerabilities.high}
'✅ No vulnerabilities' - Medium: ${metrics.weekly.vulnerabilities.medium}
}`; - Low: ${metrics.weekly.vulnerabilities.low}
await github.rest.issues.create({ ### Fix Metrics
owner: context.repo.owner, - Security PRs Submitted: ${metrics.weekly.fixes.submitted}
repo: context.repo.repo, - Automated Fixes: ${metrics.weekly.fixes.automated}
title: title, - Successfully Merged: ${metrics.weekly.fixes.merged}
body: report, - Mean Time to Fix: ${formatDuration(metrics.weekly.meanTimeToFix)}
labels: ['security', 'metrics', hasVulnerabilities ? 'attention-required' : 'healthy']
});
console.log('Successfully created trend report issue'); ### 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)
} catch (error) { > This report was automatically generated by the security metrics workflow.`;
console.error('Failed to create trend report issue:', error);
core.setFailed(`Failed to create trend report issue: ${error.message}`);
} }
collectMetrics();
- name: Cleanup - name: Cleanup
if: always() if: always()
shell: bash shell: bash

View File

@@ -4,7 +4,7 @@ name: Security Checks
on: on:
schedule: schedule:
- cron: '0 0 * * *' # Run daily at midnight - cron: '0 0 * * *' # Every day at 00:00
workflow_dispatch: workflow_dispatch:
pull_request: pull_request:
paths: paths:

View File

@@ -4,7 +4,7 @@ name: Stale
on: on:
schedule: schedule:
- cron: '0 8 * * *' - cron: '0 8 * * *' # Every day at 08:00
workflow_call: workflow_call:
workflow_dispatch: workflow_dispatch:

View File

@@ -11,7 +11,7 @@ on:
- '.github/labels.yml' - '.github/labels.yml'
- '.github/workflows/sync-labels.yml' - '.github/workflows/sync-labels.yml'
schedule: schedule:
- cron: '34 5 * * *' # 5:34 AM UTC every day - cron: '34 5 * * *' # Run every day at 05:34 AM UTC
workflow_call: workflow_call:
workflow_dispatch: workflow_dispatch:
@@ -132,7 +132,7 @@ jobs:
- name: Run Label Syncer - name: Run Label Syncer
id: sync id: sync
uses: micnncim/action-label-syncer@3abd5e6e7981d5a790c6f8a7494374bd8c74b9c6 uses: micnncim/action-label-syncer@3abd5ab72fda571e69fffd97bd4e0033dd5f495c # v1.3.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:

75
run.sh
View File

@@ -1,5 +1,29 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Error handling
set -euo pipefail
# Log file
log_file="update_$(date +%Y%m%d_%H%M%S).log"
exec 1> >(tee -a "$log_file") 2>&1
# Error handling function
handle_error() {
echo "❌ Error on line $1" | tee -a "$log_file"
exit 1
}
trap 'handle_error $LINENO' ERR
echo "🚀 Aloitetaan päivitys $(date)"
# Check required tools
for cmd in npx sed find grep; do
if ! command -v $cmd &> /dev/null; then
echo "❌ Error: $cmd not found" | tee -a "$log_file"
exit 1
fi
done
# Check if the OS is macOS or Linux # Check if the OS is macOS or Linux
if [[ $OSTYPE == "darwin"* ]]; then if [[ $OSTYPE == "darwin"* ]]; then
# macOS needs -i .bak because it doesn't support -i without arguments # macOS needs -i .bak because it doesn't support -i without arguments
@@ -9,12 +33,15 @@ else
SED_CMD="sed -i" SED_CMD="sed -i"
fi fi
# Iterate over directories
echo "📂 Iterating over directories..."
find . -mindepth 1 -maxdepth 1 -type d | while read -r dir; do find . -mindepth 1 -maxdepth 1 -type d | while read -r dir; do
dir=${dir#./} dir=${dir#./}
action="./$dir/action.yml" action="./$dir/action.yml"
# if action doesn't exist, skip # if action doesn't exist, skip
if [ ! -f "$action" ]; then if [ ! -f "$action" ]; then
echo "⏩ Skipping $dir - action.yml missing"
continue continue
fi fi
@@ -25,40 +52,58 @@ find . -mindepth 1 -maxdepth 1 -type d | while read -r dir; do
# if version doesn't exist, use 'main' # if version doesn't exist, use 'main'
if [ -z "$version" ]; then if [ -z "$version" ]; then
version="main" version="main"
echo " Version not set in $dir/action.yml, using 'main'"
fi fi
echo "Updating $readme..." echo "📝 Updating $readme..."
printf "# %s\n\n" "$repo" >"$readme" printf "# %s\n\n" "$repo" >"$readme"
echo "- Generating action documentation..." echo "📄 Generating action documentation..."
npx --yes action-docs@latest \ if ! npx --yes action-docs@latest \
--source="$action" \ --source="$action" \
--no-banner \ --no-banner \
--include-name-header >>"$readme" --include-name-header >>"$readme"; then
echo "⚠️ Warning: action-docs failed in $dir directory" | tee -a "$log_file"
fi
echo "- Replacing placeholders in $readme..." echo "🔄 Replacing placeholders in $readme..."
$SED_CMD "s|PROJECT|$repo|g; s|VERSION|$version|g; s|\*\*\*||g" "$readme" $SED_CMD "s|PROJECT|$repo|g; s|VERSION|$version|g; s|\*\*\*||g" "$readme"
if [ -f "$readme.bak" ]; then if [ -f "$readme.bak" ]; then
rm "$readme.bak" rm "$readme.bak"
echo "- Removed $readme.bak" echo "🗑️ Removed $readme.bak"
fi fi
done done
echo "" echo ""
echo "Running markdownlint..." echo "🔍 Running markdownlint..."
npx --yes markdownlint-cli --fix \ if ! npx --yes markdownlint-cli --fix \
--ignore "**/node_modules/**" "**/README.md" --ignore "**/node_modules/**" "**/README.md"; then
echo "⚠️ Warning: markdownlint found issues" | tee -a "$log_file"
fi
echo "" echo ""
echo "Running prettier..." echo "Running prettier..."
npx --yes prettier --write \ if ! npx --yes prettier --write \
"run.sh", "**/README.md" "**/action.yml" ".github/workflows/*.yml" "run.sh" "**/README.md" "**/action.yml" ".github/workflows/*.yml"; then
echo "⚠️ Warning: prettier formatting failed" | tee -a "$log_file"
fi
echo "" echo ""
echo "Running MegaLinter..." echo "🔎 Running MegaLinter..."
npx --yes mega-linter-runner if ! npx --yes mega-linter-runner; then
echo "⚠️ Warning: MegaLinter found issues" | tee -a "$log_file"
fi
echo "" echo ""
echo "Done!" # Summary report
echo "📊 Summary $(date):"
echo "- Log file: $log_file"
if [ -f "$log_file" ]; then
warnings=$(grep -c "⚠️ Warning" "$log_file" || true)
echo "- Warnings: $warnings"
fi
echo "- Status: ✅ Ready"
echo "✅ Ready!"