security.yml•5.97 kB
name: Security Scanning
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Run weekly on Mondays at 10:00 UTC
- cron: '0 10 * * 1'
workflow_dispatch:
permissions:
contents: read
security-events: write
actions: read
jobs:
bandit:
name: Bandit Security Scan
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Create virtual environment
run: uv venv
- name: Install dependencies
run: uv pip install bandit[toml]
- name: Run Bandit
run: |
uv run bandit -r src/mnemex -f json -o bandit-report.json || true
- name: Upload Bandit results
uses: actions/upload-artifact@v4
if: always()
with:
name: bandit-results
path: bandit-report.json
- name: Create issues for HIGH Bandit findings
if: always()
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const path = 'bandit-report.json';
if (!fs.existsSync(path)) {
core.info('No bandit-report.json found');
return;
}
const data = JSON.parse(fs.readFileSync(path, 'utf8'));
const results = Array.isArray(data.results) ? data.results : [];
const highs = results.filter(r => (r.issue_severity || '').toUpperCase() === 'HIGH');
core.info(`Found ${highs.length} HIGH severity findings`);
for (const r of highs) {
const testId = r.test_id || 'UNKNOWN';
const file = r.filename || 'unknown-file';
const line = r.line_number || 0;
const title = `[Bandit:${testId}] ${file}:${line}`;
const body = [
`Bandit detected a HIGH severity issue.`,
'',
`- Rule: ${testId}`,
`- File: ${file}`,
`- Line: ${line}`,
`- Severity: ${r.issue_severity}`,
`- Confidence: ${r.issue_confidence}`,
`- More info: ${r.more_info || 'n/a'}`,
'',
'Issue text:',
'```',
(r.issue_text || '').trim(),
'```'
].join('\n');
// Check if an open issue with same title already exists
const { data: existing } = await github.rest.search.issuesAndPullRequests({
q: `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open in:title "${title.replace(/"/g, '\\"')}"`
});
if (existing.total_count > 0) {
core.info(`Issue already exists for ${title}`);
continue;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels: ['security', 'bandit']
});
core.info(`Created issue: ${title}`);
}
pip-audit:
name: Dependency Vulnerability Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Create virtual environment
run: uv venv
- name: Install dependencies
run: |
uv pip install -e ".[dev]"
uv pip install pip-audit
- name: Run pip-audit
continue-on-error: true
run: |
uv run pip-audit --desc --format json --output pip-audit-report.json || true
uv run pip-audit --desc
- name: Upload pip-audit results
uses: actions/upload-artifact@v4
if: always()
with:
name: pip-audit-results
path: pip-audit-report.json
codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: python
queries: security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:python"
sbom:
name: Generate SBOM (CycloneDX)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Create virtual environment
run: uv venv
- name: Install dependencies
run: |
uv pip install -e ".[dev]"
uv pip install cyclonedx-bom
- name: Generate SBOM (JSON)
run: |
uv run cyclonedx-py environment --output-format JSON --output-file sbom.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: cyclonedx-sbom
path: sbom.json
security-summary:
name: Security Summary
runs-on: ubuntu-latest
needs: [bandit, pip-audit, codeql, sbom]
if: always()
steps:
- name: Check scan results
run: |
echo "## Security Scan Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "✅ All security scans completed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Bandit: ${{ needs.bandit.result }}" >> $GITHUB_STEP_SUMMARY
echo "- pip-audit: ${{ needs.pip-audit.result }}" >> $GITHUB_STEP_SUMMARY
echo "- CodeQL: ${{ needs.codeql.result }}" >> $GITHUB_STEP_SUMMARY
echo "- SBOM (CycloneDX): ${{ needs.sbom.result }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Review detailed results in the workflow artifacts and Security tab." >> $GITHUB_STEP_SUMMARY