mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-08-11 00:30:12 -04:00
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).
443 lines
16 KiB
YAML
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');
|
|
}
|