bambuddy/.github/workflows/security.yml
maziggy 8600b46d6c ci: upgrade setuptools in pip-audit jobs to clear PYSEC-2026-3447
The GitHub runner's Python toolcache ships setuptools 79.0.1, which
pip-audit flags for PYSEC-2026-3447 (fixed in 83.0.0), failing the
blocking Backend Security job. A fix version exists, so upgrade
setuptools in the install step rather than --ignore-vuln. Applied to
both ci.yml (blocking) and security.yml (scheduled scan).
2026-07-17 08:59:06 +02:00

443 lines
16 KiB
YAML

name: Security Audit
on:
schedule:
# Run weekly on Monday at 6:00 UTC
- cron: '0 6 * * 1'
push:
paths:
- 'backend/**'
- 'frontend/**'
- 'spoolbuddy/**'
- 'Dockerfile'
- 'docker-compose*.yml'
- 'requirements.txt'
- 'frontend/package*.json'
- '.github/workflows/security.yml'
pull_request:
paths:
- 'backend/**'
- 'frontend/**'
- 'spoolbuddy/**'
- 'Dockerfile'
- 'docker-compose*.yml'
- 'requirements.txt'
- 'frontend/package*.json'
- '.github/workflows/security.yml'
workflow_dispatch:
# Allow manual trigger
env:
PYTHON_VERSION: '3.11'
NODE_VERSION: '22'
# Default permissions for all jobs
permissions:
contents: read
jobs:
bandit:
name: Python Security Analysis (Bandit)
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install Bandit
run: pip install bandit[sarif]
- name: Run Bandit
run: |
bandit -r backend/ -f sarif -o bandit-results.sarif --severity-level medium || true
- name: Upload Bandit results to GitHub Security
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: bandit-results.sarif
category: bandit
trivy:
name: Container Security Scan (Trivy)
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v6
- name: Build Docker image
run: docker build -t bambuddy:security-scan .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@v0.35.0
with:
image-ref: 'bambuddy:security-scan'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
version: 'v0.69.1'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-results.sarif') != ''
with:
sarif_file: trivy-results.sarif
category: trivy
- name: Run Trivy for Dockerfile/IaC
uses: aquasecurity/trivy-action@v0.35.0
with:
scan-type: 'config'
scan-ref: '.'
format: 'sarif'
output: 'trivy-config-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM'
version: 'v0.69.1'
- name: Upload Trivy config results
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('trivy-config-results.sarif') != ''
with:
sarif_file: trivy-config-results.sarif
category: trivy-config
backend-audit:
name: Backend Security Audit
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
# Upgrade setuptools too: the runner's Python toolcache ships an old
# setuptools that trips pip-audit (PYSEC-2026-3447, fixed in 83.0.0).
# A fix exists, so we upgrade rather than --ignore-vuln.
python -m pip install --upgrade pip setuptools
pip install -r requirements.txt
pip install pip-audit
- name: Run pip-audit
id: pip-audit
run: |
# CVE-2025-45768 (PYSEC-2025-183 / GHSA-65pc-fj4g-8rjx): disputed by PyJWT maintainers.
# Advisory says "key length is chosen by the application that uses the library" — no
# PyJWT fix exists or will exist. Bambuddy is safe: backend/app/core/auth.py:184 uses
# secrets.token_urlsafe(64) (~86 chars of entropy) for auto-generated secrets and
# rejects file-loaded secrets shorter than 32 chars at :177. Keep ignored permanently.
pip-audit --desc on --format json --output pip-audit-results.json \
--ignore-vuln CVE-2025-45768 \
|| echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT
pip-audit --desc on \
--ignore-vuln CVE-2025-45768 \
|| true
- name: Upload audit results
if: always()
uses: actions/upload-artifact@v7
with:
name: pip-audit-results
path: pip-audit-results.json
retention-days: 30
- name: Create or close pip security issue
if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
// Check for existing open issue
const existingIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'security,automated'
});
const existingIssue = existingIssues.data.find(i => i.title.startsWith('Security Alert:') && i.title.includes('Python'));
// If no vulnerabilities found, auto-close any stale issue
if ('${{ steps.pip-audit.outputs.vulnerabilities_found }}' !== 'true') {
if (existingIssue) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: 'All Python vulnerabilities have been resolved. Closing automatically.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
state: 'closed'
});
console.log(`Auto-closed resolved issue #${existingIssue.number}`);
}
return;
}
let results;
try {
results = JSON.parse(fs.readFileSync('pip-audit-results.json', 'utf8'));
} catch {
console.log('Could not read audit results');
return;
}
// Build vulnerability table
let table = '| Package | Version | Vulnerability | Fix Version |\n';
table += '|---------|---------|---------------|-------------|\n';
for (const vuln of results.dependencies || []) {
for (const v of vuln.vulns || []) {
table += `| ${vuln.name} | ${vuln.version} | ${v.id} | ${v.fix_versions?.join(', ') || 'N/A'} |\n`;
}
}
const vulnCount = results.dependencies?.reduce((acc, d) => acc + (d.vulns?.length || 0), 0) || 0;
if (vulnCount === 0) {
console.log('No vulnerabilities to report');
if (existingIssue) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: 'All Python vulnerabilities have been resolved. Closing automatically.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
state: 'closed'
});
console.log(`Auto-closed resolved issue #${existingIssue.number}`);
}
return;
}
const title = `Security Alert: ${vulnCount} Python vulnerabilities found`;
const body = `## Automated Security Audit Results
The weekly security audit found vulnerabilities in Python dependencies.
${table}
### Recommended Actions
1. Review each vulnerability
2. Update affected packages: \`pip install --upgrade <package>\`
3. Run \`pip-audit\` locally to verify fixes
---
*This issue was automatically created by the security audit workflow.*`;
if (existingIssue) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: body
});
console.log(`Updated existing issue #${existingIssue.number}`);
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['security', 'automated', 'dependencies']
});
console.log('Created new security issue');
}
frontend-audit:
name: Frontend Security Audit
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Run npm audit
id: npm-audit
working-directory: frontend
run: |
npm audit --omit=dev --json > npm-audit-raw.json 2>/dev/null || true
# Filter audit results to only include actual project dependencies.
# npm 10.x audit/ls reports vulns in its own bundled deps (npm, tar, minimatch)
# so we parse package-lock.json directly to get the real prod dep list.
node -e "
const fs = require('fs');
const raw = fs.readFileSync('npm-audit-raw.json', 'utf8');
let results;
try { results = JSON.parse(raw); } catch { results = { vulnerabilities: {} }; }
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
const prodDeps = new Set();
for (const [path, info] of Object.entries(lock.packages || {})) {
if (path && !info.dev && !info.devOptional) {
prodDeps.add(path.split('node_modules/').pop());
}
}
const vulns = results.vulnerabilities || {};
const filtered = {};
for (const [name, info] of Object.entries(vulns)) {
if (prodDeps.has(name)) filtered[name] = info;
}
results.vulnerabilities = filtered;
fs.writeFileSync('npm-audit-results.json', JSON.stringify(results, null, 2));
const count = Object.keys(filtered).length;
console.log(count > 0
? count + ' production vulnerabilities found'
: 'No production vulnerabilities (filtered ' + Object.keys(vulns).length + ' npm-internal entries)');
if (count > 0) process.exit(1);
" || echo "vulnerabilities_found=true" >> $GITHUB_OUTPUT
npm audit --omit=dev --audit-level=high || true
- name: Upload audit results
if: always()
uses: actions/upload-artifact@v7
with:
name: npm-audit-results
path: frontend/npm-audit-results.json
retention-days: 30
- name: Create or close npm security issue
if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
// Check for existing open issue
const existingIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'security,automated'
});
const existingIssue = existingIssues.data.find(i => i.title.startsWith('Security Alert:') && i.title.includes('npm'));
// If filter didn't flag vulnerabilities, auto-close any stale issue
if ('${{ steps.npm-audit.outputs.vulnerabilities_found }}' !== 'true') {
if (existingIssue) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: 'All npm production vulnerabilities have been resolved. Closing automatically.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
state: 'closed'
});
console.log(`Auto-closed resolved issue #${existingIssue.number}`);
}
return;
}
let results;
try {
results = JSON.parse(fs.readFileSync('frontend/npm-audit-results.json', 'utf8'));
} catch {
console.log('Could not read filtered audit results');
return;
}
const vulns = results.vulnerabilities || {};
const vulnCount = Object.keys(vulns).length;
if (vulnCount === 0) {
console.log('No vulnerabilities to report');
if (existingIssue) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: 'All npm production vulnerabilities have been resolved. Closing automatically.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
state: 'closed'
});
console.log(`Auto-closed resolved issue #${existingIssue.number}`);
}
return;
}
// Build vulnerability table
let table = '| Package | Severity | Via | Fix |\n';
table += '|---------|----------|-----|-----|\n';
for (const [name, info] of Object.entries(vulns)) {
const via = Array.isArray(info.via) ? info.via.map(v => typeof v === 'string' ? v : v.name).join(', ') : info.via;
table += `| ${name} | ${info.severity} | ${via} | ${info.fixAvailable ? 'Yes' : 'No'} |\n`;
}
const title = `Security Alert: ${vulnCount} npm vulnerabilities found`;
const body = `## Automated Security Audit Results
The weekly security audit found vulnerabilities in npm dependencies.
${table}
### Recommended Actions
1. Review each vulnerability: \`npm audit\`
2. Auto-fix if possible: \`npm audit fix\`
3. Manual fix for breaking changes: \`npm audit fix --force\` (review changes!)
---
*This issue was automatically created by the security audit workflow.*`;
if (existingIssue) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: body
});
console.log(`Updated existing issue #${existingIssue.number}`);
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['security', 'automated', 'dependencies']
});
console.log('Created new security issue');
}