Skip to content

Repository Security Scan #35

Repository Security Scan

Repository Security Scan #35

name: Repository Security Scan
on:
push:
branches: [main, develop]
paths-ignore: ['**.md', 'docs/**', '.gitignore']
pull_request:
branches: [main, develop]
paths-ignore: ['**.md', 'docs/**', '.gitignore']
schedule:
- cron: '0 2 * * 1' # Weekly security scan
workflow_dispatch:
inputs:
fail_on_critical:
description: 'Fail on critical findings'
required: false
default: true
type: boolean
permissions:
contents: read
security-events: write
pull-requests: write
packages: read
actions: read
jobs:
security-scan:
name: Security Scan
runs-on: ubuntu-latest
outputs:
scan-status: ${{ steps.scan.outputs.status }}
risk-level: ${{ steps.scan.outputs.risk-level }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Determine scanner image to use
id: scanner-image
run: |
# Determine the appropriate Docker tag based on the event and branch
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
TAG="pr-${{ github.event.number }}"
elif [[ "${{ github.ref_name }}" == "main" ]]; then
TAG="latest"
else
# Sanitize branch name for Docker tag (replace / with -)
TAG=$(echo "${{ github.ref_name }}" | sed 's/\//-/g')
fi
PRIMARY_IMAGE="ghcr.io/${{ github.repository_owner }}/git-security-scanner:${TAG}"
FALLBACK_IMAGE="ghcr.io/${{ github.repository_owner }}/git-security-scanner:latest"
echo "Checking if primary scanner image is available: ${PRIMARY_IMAGE}"
if docker manifest inspect "${PRIMARY_IMAGE}" > /dev/null 2>&1; then
echo "✅ Primary scanner image is available!"
echo "image=${PRIMARY_IMAGE}" >> $GITHUB_OUTPUT
else
echo "⚠️ Primary image not found, checking fallback: ${FALLBACK_IMAGE}"
if docker manifest inspect "${FALLBACK_IMAGE}" > /dev/null 2>&1; then
echo "✅ Fallback scanner image is available!"
echo "image=${FALLBACK_IMAGE}" >> $GITHUB_OUTPUT
else
echo "❌ Neither primary nor fallback image available"
exit 1
fi
fi
- name: Prepare scan results directory
run: |
# Create directory with proper permissions for container user
mkdir -p /tmp/scan-results
chmod 777 /tmp/scan-results
- name: Run security scan
id: scan
run: |
# Scanner handles all configuration internally
docker run --rm \
-v ${{ github.workspace }}:/scan:ro \
-v /tmp/scan-results:/reports \
-e GITHUB_ACTIONS=true \
-e FAIL_ON_CRITICAL=${{ github.event.inputs.fail_on_critical || 'true' }} \
-e GITHUB_REPOSITORY="${{ github.repository }}" \
-e GITHUB_REF="${{ github.ref }}" \
-e GITHUB_SHA="${{ github.sha }}" \
${{ steps.scanner-image.outputs.image }}
# Extract results for GitHub Actions
if [[ -f "/tmp/scan-results/json/final-security-report.json" ]]; then
RISK_LEVEL=$(jq -r '.metadata.risk_level // "UNKNOWN"' /tmp/scan-results/json/final-security-report.json 2>/dev/null || echo "UNKNOWN")
echo "status=completed" >> $GITHUB_OUTPUT
echo "risk-level=${RISK_LEVEL}" >> $GITHUB_OUTPUT
cp /tmp/scan-results/json/final-security-report.json /tmp/scan-results/summary.json
else
echo "status=failed" >> $GITHUB_OUTPUT
echo "risk-level=UNKNOWN" >> $GITHUB_OUTPUT
fi
# Stricter check for main branch
if [[ "${{ github.ref }}" == "refs/heads/main" && "$HIGH_COUNT" -gt "5" ]]; then
echo "❌ Quality gate failed: Too many high-severity issues (${HIGH_COUNT}) for main branch"
exit 1
fi
echo "✅ Quality gates passed"
- name: Upload SARIF results to GitHub (optional)
if: always() && vars.ENABLE_CODEQL_UPLOAD == 'true'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: /tmp/scan-results/*.sarif
category: 'git-security-scanner'
continue-on-error: true
- name: Upload scan artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: security-scan-results
path: /tmp/scan-results/
retention-days: 30
- name: Comment on PR
if: github.event_name == 'pull_request' && always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let summary = {};
try {
if (fs.existsSync('/tmp/scan-results/summary.json')) {
summary = JSON.parse(fs.readFileSync('/tmp/scan-results/summary.json', 'utf8'));
}
} catch (e) {
console.log('Could not read summary:', e);
}
const riskLevel = summary.metadata?.risk_level || 'UNKNOWN';
const critical = summary.metadata?.critical_issues || 0;
const high = summary.metadata?.high_issues || 0;
const medium = summary.metadata?.medium_issues || 0;
const low = summary.metadata?.low_issues || 0;
const statusEmoji = {'CRITICAL': '🚨', 'HIGH': '⚠️', 'MEDIUM': '🟡', 'LOW': '🟢', 'UNKNOWN': '❓'}[riskLevel] || '❓';
const comment = `## ${statusEmoji} Security Scan Results
**Risk Level**: ${riskLevel}
| Severity | Count | Status |
|----------|-------|--------|
| 🚨 Critical | ${critical} | ${critical === 0 ? '✅' : '❌'} |
| ⚠️ High | ${high} | ${high === 0 ? '✅' : '⚠️'} |
| 🟡 Medium | ${medium} | ${medium <= 5 ? '✅' : 'ℹ️'} |
| 🔵 Low | ${low} | ℹ️ |
${critical > 0 ? '### ⚠️ Action Required\nCritical vulnerabilities must be fixed before merge.\n' : ''}
[View Full Report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
---
*Powered by Security Scanner | Run #${{ github.run_number }}*`;
// Update or create comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c => c.body.includes('Security Scan Results'));
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}