Files
actions/.github/workflows/security-suite.yml
renovate[bot] 591042cb3b chore(deps): update github/codeql-action action (v3.30.1 → v3.30.3) (#251)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-15 11:03:29 +03:00

356 lines
12 KiB
YAML

---
# 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
jobs:
check-secrets:
name: Check Required Secrets
runs-on: ubuntu-latest
outputs:
run_snyk: ${{ steps.check.outputs.run_snyk }}
run_slack: ${{ steps.check.outputs.run_slack }}
run_sonarcloud: ${{ steps.check.outputs.run_sonarcloud }}
steps:
- name: Check Required Secrets
id: check
shell: bash
run: |
{
echo "run_snyk=false"
echo "run_slack=false"
echo "run_sonarcloud=false"
} >> "$GITHUB_OUTPUT"
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
owasp:
name: OWASP Dependency Check
runs-on: ubuntu-latest
needs: check-secrets
permissions:
security-events: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- 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@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
with:
sarif_file: reports/dependency-check-report.sarif
category: owasp-dependency-check
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: owasp-results
path: reports/dependency-check-report.sarif
snyk:
name: Snyk Security Scan
runs-on: ubuntu-latest
needs: check-secrets
if: needs.check-secrets.outputs.run_snyk == 'true'
permissions:
security-events: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 'lts/*'
cache: 'npm'
- name: Run Snyk Scan
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
uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
with:
sarif_file: snyk-results.sarif
category: snyk
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: snyk-results
path: snyk-results.sarif
scorecard:
name: OSSF Scorecard
runs-on: ubuntu-latest
needs: check-secrets
permissions:
security-events: write
id-token: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Run Scorecard
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with:
results_file: scorecard-results.sarif
results_format: sarif
publish_results: true
- name: Upload Scorecard Results
uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
with:
sarif_file: scorecard-results.sarif
category: scorecard
- name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: scorecard-results
path: scorecard-results.sarif
analyze:
name: Analyze Results
runs-on: ubuntu-latest
needs: [check-secrets, owasp, scorecard, snyk]
if: always()
permissions:
issues: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download scan results
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
path: ./results
- name: Analyze Results
id: analysis
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
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('./results/owasp-results/dependency-check-report.sarif', 'OWASP'),
snyk: ${{ needs.check-secrets.outputs.run_snyk == 'true' }} ?
analyzeSarif('./results/snyk-results/snyk-results.sarif', 'Snyk') : null,
scorecard: analyzeSarif('./results/scorecard-results/scorecard-results.sarif', 'Scorecard')
};
// Save results
fs.writeFileSync('security-results.json', JSON.stringify(metrics, null, 2));
// Set outputs
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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: security-results
path: |
reports/
*.sarif
security-results.json
retention-days: 30
- name: Notify on Failure
if: failure() && needs.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 }}