Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions .github/workflows/security-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
name: Security CI/CD

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read
security-events: write

jobs:
security-lint:
name: Python Security Linting (Bandit)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install bandit
run: pip install bandit[toml]
- name: Run bandit security linter
run: bandit -r backend/secuscan -f json -o bandit-report.json --severity-level medium || true
- name: Check for high-severity findings
run: |
python -c "
import json, sys
with open('bandit-report.json') as f:
data = json.load(f)
high = [r for r in data.get('results', []) if r.get('issue_severity') == 'HIGH']
if high:
print('::error::Found {} HIGH severity security issues'.format(len(high)))
for r in high:
print(' - {}:{}: {}'.format(r['filename'], r['line_number'], r['issue_text']))
sys.exit(1)
print('No HIGH severity issues found')
"
- name: Upload bandit report
if: always()
uses: actions/upload-artifact@v4
with:
name: bandit-report
path: bandit-report.json

auth-protection-check:
name: Route Authentication Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Check all API routers have authentication
run: |
python -c "
import re, sys
from pathlib import Path
vulns = []
for py_file in Path('backend/secuscan').rglob('*.py'):
content = py_file.read_text()
router_pattern = re.compile(r'APIRouter\((.*?)\)', re.DOTALL)
for match in router_pattern.finditer(content):
args = match.group(1)
if 'tags' in args and 'auth' not in args.lower() and 'health' not in args.lower():
if 'dependencies' not in args or 'require_api_key' not in args:
line_num = content[:match.start()].count('\n') + 1
vulns.append(' {}:{} - Router missing require_api_key dependency'.format(py_file, line_num))
if vulns:
print('::warning::API routers without authentication dependency:')
for v in vulns:
print(v)
print()
print('If these are intentional (e.g., health check), ignore this warning.')
print('Otherwise, add: dependencies=[Depends(require_api_key)]')
else:
print('All API routers have authentication dependencies')
"

debug-mode-check:
name: Debug Mode Default Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Verify debug mode is off by default
run: |
python -c "
import re, sys
from pathlib import Path
issues = []
for py_file in Path('backend/secuscan').rglob('*.py'):
content = py_file.read_text()
lines = content.splitlines()
for i, line in enumerate(lines, 1):
stripped = line.strip()
# Check for debug=True defaults (not in if statements or comments)
if re.search(r'debug\s*=\s*True', stripped):
if not stripped.startswith('if') and not stripped.startswith('#'):
issues.append('{}:{}: debug defaults to True'.format(py_file, i))
# Check for traceback.format_exc in responses - verify guarded by debug check
if 'traceback.format_exc' in line:
# Look backwards up to 10 lines for an if settings.debug guard
guarded = False
for j in range(max(0, i - 11), i - 1):
prev = lines[j] if j < len(lines) else ''
if 'settings.debug' in prev and ('if' in prev or 'elif' in prev):
guarded = True
break
if not guarded:
issues.append('{}:{}: traceback exposed without debug guard'.format(py_file, i))
if issues:
print('::error::Debug mode security issues found:')
for i in issues:
print(' ' + i)
sys.exit(1)
print('Debug mode defaults verified safe')
"

secret-scan:
name: Hardcoded Secret Detection
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan for hardcoded secrets
run: |
python -c "
import re, sys
from pathlib import Path
patterns = [
(r'password\s*=\s*[\"\\x27][^\"\\x27]+[\"\\x27]', 'Hardcoded password'),
(r'api_key\s*=\s*[\"\\x27][A-Za-z0-9]{20,}[\"\\x27]', 'Hardcoded API key'),
(r'secret\s*=\s*[\"\\x27][^\"\\x27]+[\"\\x27]', 'Hardcoded secret'),
(r'token\s*=\s*[\"\\x27][A-Za-z0-9]{20,}[\"\\x27]', 'Hardcoded token'),
(r'POSTGRES_PASSWORD\s*:\s*secuscan', 'Default DB password'),
]
skip_files = {'.env.example', 'conftest.py'}
skip_lines = ['# ', 'Example:', 'example:', 'default', 'replace-with']
issues = []
for py_file in Path('backend').rglob('*.py'):
if any(s in py_file.name for s in skip_files) or py_file.name.startswith('test_'):
continue
content = py_file.read_text()
for i, line in enumerate(content.splitlines(), 1):
if any(s in line for s in skip_lines):
continue
for pattern, desc in patterns:
if re.search(pattern, line, re.IGNORECASE):
issues.append('{}:{}: {}'.format(py_file, i, desc))
for yml in Path('.').glob('docker-compose*.yml'):
content = yml.read_text()
for i, line in enumerate(content.splitlines(), 1):
if 'POSTGRES_PASSWORD' in line and 'secuscan' in line:
if '\${' not in line:
issues.append('{}:{}: Default database password in compose file'.format(yml, i))
if issues:
print('::warning::Potential hardcoded secrets found:')
for i in issues:
print(' ' + i)
print()
print('Review these findings. False positives are possible.')
else:
print('No hardcoded secrets detected')
"

env-config-check:
name: Environment Config Validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Validate .env.example safe defaults
run: |
python -c "
import sys
from pathlib import Path
env_file = Path('.env.example')
if not env_file.exists():
print('.env.example not found, skipping')
sys.exit(0)
content = env_file.read_text()
issues = []
checks = [
('SECUSCAN_DEBUG=true', 'SECUSCAN_DEBUG should default to false for safety'),
]
for bad, msg in checks:
if bad in content:
issues.append(msg)
if issues:
print('::warning::.env.example configuration issues:')
for i in issues:
print(' - ' + i)
else:
print('.env.example configuration validated')
"

dependency-audit:
name: Dependency Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install pip-audit
run: pip install pip-audit
- name: Audit Python dependencies
run: |
pip-audit -r backend/requirements.txt --desc --format json > dep-audit.json || true
python -c "
import json, sys
with open('dep-audit.json') as f:
data = json.load(f)
vulns = data.get('dependencies', [])
critical = []
for dep in vulns:
for v in dep.get('vulns', []):
sev = v.get('fix_versions', [])
if sev:
critical.append('{}=={}: {}'.format(dep['name'], dep['version'], v.get('id', 'unknown')))
if critical:
print('::warning::Found {} vulnerable dependencies:'.format(len(critical)))
for c in critical[:10]:
print(' ' + c)
else:
print('No known vulnerable dependencies found')
"
- name: Upload audit report
if: always()
uses: actions/upload-artifact@v4
with:
name: dep-audit-report
path: dep-audit.json
Loading