mirror of
https://github.com/ivuorinen/actions.git
synced 2026-03-02 16:53:57 +00:00
* fix(deps): replace step-security/retry with nick-fields/retry * chore(deps): update github action sha pins via pinact * refactor: remove common-retry references from tests and validators * chore: simplify description fallback and update action count * docs: remove hardcoded test counts from memory and docs Replace exact "769 tests" references with qualitative language so these files don't go stale as test count grows.
283 lines
9.7 KiB
YAML
283 lines
9.7 KiB
YAML
# yaml-language-server: $schema=https://json.schemastore.org/github-action.json
|
|
#
|
|
# REQUIRED PERMISSIONS (set these in your workflow file):
|
|
# permissions:
|
|
# security-events: write # Required for SARIF uploads
|
|
# contents: read # Required for repository access
|
|
#
|
|
---
|
|
name: Security Scan
|
|
description: |
|
|
Comprehensive security scanning for GitHub Actions including actionlint,
|
|
Gitleaks (optional), and Trivy vulnerability scanning. Requires
|
|
'security-events: write' and 'contents: read' permissions in the workflow.
|
|
author: Ismo Vuorinen
|
|
branding:
|
|
icon: shield
|
|
color: red
|
|
|
|
inputs:
|
|
gitleaks-license:
|
|
description: 'Gitleaks license key (required for Gitleaks scanning)'
|
|
required: false
|
|
default: ''
|
|
gitleaks-config:
|
|
description: 'Path to Gitleaks config file'
|
|
required: false
|
|
default: '.gitleaks.toml'
|
|
trivy-severity:
|
|
description: 'Severity levels to scan for (comma-separated)'
|
|
required: false
|
|
default: 'CRITICAL,HIGH'
|
|
trivy-scanners:
|
|
description: 'Types of scanners to run (comma-separated)'
|
|
required: false
|
|
default: 'vuln,config,secret'
|
|
trivy-timeout:
|
|
description: 'Timeout for Trivy scan'
|
|
required: false
|
|
default: '10m'
|
|
actionlint-enabled:
|
|
description: 'Enable actionlint scanning'
|
|
required: false
|
|
default: 'true'
|
|
token:
|
|
description: 'GitHub token for authentication'
|
|
required: false
|
|
default: ''
|
|
|
|
outputs:
|
|
has_trivy_results:
|
|
description: 'Whether Trivy scan produced valid results'
|
|
value: ${{ steps.verify-sarif.outputs.has_trivy }}
|
|
has_gitleaks_results:
|
|
description: 'Whether Gitleaks scan produced valid results'
|
|
value: ${{ steps.verify-sarif.outputs.has_gitleaks }}
|
|
total_issues:
|
|
description: 'Total number of security issues found'
|
|
value: ${{ steps.analyze.outputs.total_issues }}
|
|
critical_issues:
|
|
description: 'Number of critical security issues found'
|
|
value: ${{ steps.analyze.outputs.critical_issues }}
|
|
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
- name: Validate Inputs
|
|
id: validate
|
|
uses: ivuorinen/actions/validate-inputs@5cc7373a22402ee8985376bc713f00e09b5b2edb
|
|
with:
|
|
action-type: security-scan
|
|
gitleaks-license: ${{ inputs.gitleaks-license }}
|
|
gitleaks-config: ${{ inputs.gitleaks-config }}
|
|
trivy-severity: ${{ inputs.trivy-severity }}
|
|
trivy-scanners: ${{ inputs.trivy-scanners }}
|
|
trivy-timeout: ${{ inputs.trivy-timeout }}
|
|
actionlint-enabled: ${{ inputs.actionlint-enabled }}
|
|
token: ${{ inputs.token }}
|
|
|
|
- name: Check Required Configurations
|
|
id: check-configs
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
|
|
# Initialize all flags as false
|
|
{
|
|
printf '%s\n' "run_gitleaks=false"
|
|
printf '%s\n' "run_trivy=true"
|
|
printf '%s\n' "run_actionlint=${{ inputs.actionlint-enabled }}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
# Check Gitleaks configuration and license
|
|
if [ -f "${{ inputs.gitleaks-config }}" ] && [ -n "${{ inputs.gitleaks-license }}" ]; then
|
|
printf 'Gitleaks config and license found\n'
|
|
printf '%s\n' "run_gitleaks=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
printf '::warning::Gitleaks config or license missing - skipping Gitleaks scan\n'
|
|
fi
|
|
|
|
- name: Run actionlint
|
|
if: steps.check-configs.outputs.run_actionlint == 'true'
|
|
uses: raven-actions/actionlint@e01d1ea33dd6a5ed517d95b4c0c357560ac6f518 # v2.1.1
|
|
with:
|
|
cache: true
|
|
fail-on-error: true
|
|
shellcheck: false
|
|
|
|
- name: Run Gitleaks
|
|
if: steps.check-configs.outputs.run_gitleaks == 'true'
|
|
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
|
|
env:
|
|
GITHUB_TOKEN: ${{ inputs.token || github.token }}
|
|
GITLEAKS_LICENSE: ${{ inputs.gitleaks-license }}
|
|
with:
|
|
config-path: ${{ inputs.gitleaks-config }}
|
|
report-format: sarif
|
|
report-path: gitleaks-report.sarif
|
|
|
|
- name: Run Trivy vulnerability scanner
|
|
if: steps.check-configs.outputs.run_trivy == 'true'
|
|
uses: aquasecurity/trivy-action@a11da62073708815958ea6d84f5650c78a3ef85b # master
|
|
with:
|
|
scan-type: 'fs'
|
|
scanners: ${{ inputs.trivy-scanners }}
|
|
format: 'sarif'
|
|
output: 'trivy-results.sarif'
|
|
severity: ${{ inputs.trivy-severity }}
|
|
timeout: ${{ inputs.trivy-timeout }}
|
|
|
|
- name: Verify SARIF files
|
|
id: verify-sarif
|
|
shell: sh
|
|
run: |
|
|
set -eu
|
|
|
|
# Initialize outputs
|
|
{
|
|
printf '%s\n' "has_trivy=false"
|
|
printf '%s\n' "has_gitleaks=false"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
# Check Trivy results
|
|
if [ -f "trivy-results.sarif" ]; then
|
|
if jq -e . <"trivy-results.sarif" >/dev/null 2>&1; then
|
|
printf '%s\n' "has_trivy=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
printf '::warning::Trivy SARIF file exists but is not valid JSON\n'
|
|
fi
|
|
fi
|
|
|
|
# Check Gitleaks results if it ran
|
|
if [ "${{ steps.check-configs.outputs.run_gitleaks }}" = "true" ]; then
|
|
if [ -f "gitleaks-report.sarif" ]; then
|
|
if jq -e . <"gitleaks-report.sarif" >/dev/null 2>&1; then
|
|
printf '%s\n' "has_gitleaks=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
printf '::warning::Gitleaks SARIF file exists but is not valid JSON\n'
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
- name: Upload Trivy results
|
|
if: steps.verify-sarif.outputs.has_trivy == 'true'
|
|
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
|
with:
|
|
sarif_file: 'trivy-results.sarif'
|
|
category: 'trivy'
|
|
|
|
- name: Upload Gitleaks results
|
|
if: steps.verify-sarif.outputs.has_gitleaks == 'true'
|
|
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
|
with:
|
|
sarif_file: 'gitleaks-report.sarif'
|
|
category: 'gitleaks'
|
|
|
|
- name: Archive security reports
|
|
if: always()
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: security-reports-${{ github.run_id }}
|
|
path: |
|
|
${{ steps.verify-sarif.outputs.has_trivy == 'true' && 'trivy-results.sarif' || '' }}
|
|
${{ steps.verify-sarif.outputs.has_gitleaks == 'true' && 'gitleaks-report.sarif' || '' }}
|
|
retention-days: 30
|
|
|
|
- name: Analyze Results
|
|
id: analyze
|
|
if: always()
|
|
shell: node {0}
|
|
run: |
|
|
const fs = require('fs');
|
|
|
|
try {
|
|
let totalIssues = 0;
|
|
let criticalIssues = 0;
|
|
|
|
const analyzeSarif = (file, tool) => {
|
|
if (!fs.existsSync(file)) {
|
|
console.log(`No results file found for ${tool}`);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const sarif = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
return sarif.runs.reduce((acc, run) => {
|
|
if (!run.results) return acc;
|
|
|
|
const critical = run.results.filter(r =>
|
|
r.level === 'error' ||
|
|
r.level === 'critical' ||
|
|
(r.ruleId || '').toLowerCase().includes('critical')
|
|
).length;
|
|
|
|
return {
|
|
total: acc.total + run.results.length,
|
|
critical: acc.critical + critical
|
|
};
|
|
}, { total: 0, critical: 0 });
|
|
} catch (error) {
|
|
console.log(`Error analyzing ${tool} results: ${error.message}`);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Only analyze results from tools that ran successfully
|
|
const results = {
|
|
trivy: '${{ steps.verify-sarif.outputs.has_trivy }}' === 'true' ?
|
|
analyzeSarif('trivy-results.sarif', 'trivy') : null,
|
|
gitleaks: '${{ steps.verify-sarif.outputs.has_gitleaks }}' === 'true' ?
|
|
analyzeSarif('gitleaks-report.sarif', 'gitleaks') : null
|
|
};
|
|
|
|
// Aggregate results
|
|
Object.entries(results).forEach(([tool, result]) => {
|
|
if (result) {
|
|
totalIssues += result.total;
|
|
criticalIssues += result.critical;
|
|
console.log(`${tool}: ${result.total} total, ${result.critical} critical issues`);
|
|
}
|
|
});
|
|
|
|
// Create summary
|
|
const summary = `## Security Scan Summary
|
|
|
|
- Total Issues Found: ${totalIssues}
|
|
- Critical Issues: ${criticalIssues}
|
|
|
|
### Tool Breakdown
|
|
${Object.entries(results)
|
|
.filter(([_, r]) => r)
|
|
.map(([tool, r]) =>
|
|
`- ${tool}: ${r.total} total, ${r.critical} critical`
|
|
).join('\n')}
|
|
|
|
### Tools Run Status
|
|
- Actionlint: ${{ steps.check-configs.outputs.run_actionlint }}
|
|
- Trivy: ${{ steps.verify-sarif.outputs.has_trivy }}
|
|
- Gitleaks: ${{ steps.check-configs.outputs.run_gitleaks }}
|
|
`;
|
|
|
|
// Set outputs using GITHUB_OUTPUT
|
|
const outputFile = process.env.GITHUB_OUTPUT;
|
|
if (outputFile) {
|
|
fs.appendFileSync(outputFile, `total_issues=${totalIssues}\n`);
|
|
fs.appendFileSync(outputFile, `critical_issues=${criticalIssues}\n`);
|
|
}
|
|
|
|
// Add job summary using GITHUB_STEP_SUMMARY
|
|
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
|
if (summaryFile) {
|
|
fs.appendFileSync(summaryFile, summary + '\n');
|
|
}
|
|
|
|
// Fail if critical issues found
|
|
if (criticalIssues > 0) {
|
|
console.error(`Found ${criticalIssues} critical security issues`);
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Analysis failed: ${error.message}`);
|
|
process.exit(1);
|
|
}
|