-
Notifications
You must be signed in to change notification settings - Fork 249
240 lines (232 loc) · 9.73 KB
/
Copy pathsecurity-ci.yml
File metadata and controls
240 lines (232 loc) · 9.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
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@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: bandit-report
path: bandit-report.json
auth-protection-check:
name: Route Authentication Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@11d5960a326750d5838078e36cf38b85af677262 # 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@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dep-audit-report
path: dep-audit.json