@orionscanner/cli 0.1.0 → 0.5.1
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.
- package/README.md +80 -190
- package/bin/orion.js +30 -29
- package/dist/orion.exe +0 -0
- package/package.json +46 -48
- package/docs/API_REFERENCE.md +0 -211
- package/docs/ARCHITECTURE.md +0 -183
- package/docs/CHANGELOG.md +0 -112
- package/docs/CONTRIBUTING.md +0 -157
- package/docs/ENGINE_CATALOG.md +0 -87
- package/docs/GETTING_STARTED.md +0 -226
- package/pyproject.toml +0 -16
- package/src/orion/__init__.py +0 -0
- package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__init__.py +0 -17
- package/src/orion/engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/config.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/core.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/ir.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/languages.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/registry.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/results.cpython-313.pyc +0 -0
- package/src/orion/engine/__pycache__/runner.cpython-313.pyc +0 -0
- package/src/orion/engine/api_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/api_security/engine.py +0 -125
- package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/architecture/engine.py +0 -463
- package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/authorization/engine.py +0 -337
- package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/callgraph/engine.py +0 -236
- package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/cfg/engine.py +0 -509
- package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos/engine.py +0 -268
- package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos_config/engine.py +0 -322
- package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/complexity/engine.py +0 -144
- package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/compliance/engine.py +0 -325
- package/src/orion/engine/config.py +0 -41
- package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_engine/engine.py +0 -364
- package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_validation/engine.py +0 -313
- package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/container/engine.py +0 -546
- package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/contract_probe/engine.py +0 -316
- package/src/orion/engine/core.py +0 -75
- package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/crypto_guard/engine.py +0 -134
- package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/data_residency/engine.py +0 -238
- package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dataflow/engine.py +0 -398
- package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dead_import/engine.py +0 -293
- package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/deps/engine.py +0 -244
- package/src/orion/engine/dispatcher.py +0 -350
- package/src/orion/engine/docs/AUDIT_PROMPT.md +0 -1248
- package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/duplication/engine.py +0 -120
- package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/error_handling/engine.py +0 -256
- package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/fp_learn/engine.py +0 -248
- package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/hunter/engine.py +0 -324
- package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/iac_security/engine.py +0 -218
- package/src/orion/engine/ir.py +0 -22
- package/src/orion/engine/languages.py +0 -454
- package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/license/engine.py +0 -172
- package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/lint/engine.py +0 -558
- package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/llm_security/engine.py +0 -125
- package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/logging_c/engine.py +0 -164
- package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/noise/engine.py +0 -140
- package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/orphan/engine.py +0 -244
- package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/perf_guard/engine.py +0 -345
- package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/prompt_security/detectors/__init__.py +0 -4
- package/src/orion/engine/prompt_security/engine.py +0 -170
- package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/provenance/engine.py +0 -242
- package/src/orion/engine/registry.py +0 -55
- package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/regression/engine.py +0 -213
- package/src/orion/engine/results.py +0 -21
- package/src/orion/engine/runner.py +0 -1310
- package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sandbox_escape/engine.py +0 -284
- package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sast/engine.py +0 -217
- package/src/orion/engine/sast/patterns/__init__.py +0 -513
- package/src/orion/engine/sast/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/secrets/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/secrets/engine.py +0 -77
- package/src/orion/engine/secrets/patterns/__init__.py +0 -74
- package/src/orion/engine/secrets/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/sensitive_data/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sensitive_data/engine.py +0 -143
- package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/state/engine.py +0 -218
- package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/supply_chain_sbom/engine.py +0 -290
- package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/symbol/engine.py +0 -463
- package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/taint/engine.py +0 -464
- package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/test_coverage/engine.py +0 -445
- package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/threat_model/engine.py +0 -126
- package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/wiring_trace/engine.py +0 -200
- package/src/orion_engine.egg-info/PKG-INFO +0 -7
- package/src/orion_engine.egg-info/SOURCES.txt +0 -65
- package/src/orion_engine.egg-info/dependency_links.txt +0 -1
- package/src/orion_engine.egg-info/entry_points.txt +0 -2
- package/src/orion_engine.egg-info/top_level.txt +0 -1
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
"""Authorization engine for detecting missing authentication on routes."""
|
|
2
|
-
import ast
|
|
3
|
-
import re
|
|
4
|
-
from typing import Dict, List, Set, Optional, Any, Tuple
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from orion.engine.config import EngineConfig, ResourceLimits
|
|
7
|
-
from orion.engine.ir import OrionIR
|
|
8
|
-
from orion.engine.results import EngineResult, Finding
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
AUTH_DECORATORS = {
|
|
12
|
-
"flask": [
|
|
13
|
-
"login_required", "permission_required", "roles_required",
|
|
14
|
-
"auth_required", "token_required", "jwt_required",
|
|
15
|
-
"fresh_login_required", "current_user",
|
|
16
|
-
],
|
|
17
|
-
"fastapi": [
|
|
18
|
-
"Depends", "Security", "get_current_user", "get_current_active_user",
|
|
19
|
-
"oauth2_scheme", "http_bearer", "api_key_header",
|
|
20
|
-
],
|
|
21
|
-
"django": [
|
|
22
|
-
"login_required", "permission_required", "user_passes_test",
|
|
23
|
-
"staff_member_required", "superuser_required",
|
|
24
|
-
"LoginRequiredMixin", "PermissionRequiredMixin", "UserPassesTestMixin",
|
|
25
|
-
],
|
|
26
|
-
"bottle": ["auth_required", "require_auth"],
|
|
27
|
-
"tornado": ["authenticated", "auth_required"],
|
|
28
|
-
"starlette": ["Depends", "Security"],
|
|
29
|
-
"generic": ["requires_auth", "requires_login", "requires_permission",
|
|
30
|
-
"check_auth", "verify_token", "validate_user"],
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
ROUTE_DECORATORS = {
|
|
34
|
-
"flask": ["route", "get", "post", "put", "delete", "patch", "head", "options"],
|
|
35
|
-
"fastapi": ["get", "post", "put", "delete", "patch", "head", "options", "api_route"],
|
|
36
|
-
"django": ["path", "re_path", "url"],
|
|
37
|
-
"bottle": ["route", "get", "post", "put", "delete", "patch"],
|
|
38
|
-
"tornado": ["route"],
|
|
39
|
-
"starlette": ["route", "get", "post", "put", "delete", "patch"],
|
|
40
|
-
"aiohttp": ["get", "post", "put", "delete", "patch"],
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
EXEMPT_PATTERNS = [
|
|
44
|
-
r"/health", r"/ready", r"/live", r"/metrics", r"/status",
|
|
45
|
-
r"/ping", r"/version", r"/favicon", r"/robots\.txt",
|
|
46
|
-
r"/static/", r"/media/", r"/assets/",
|
|
47
|
-
r"\.well-known/", r"/docs", r"/redoc", r"/openapi",
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@dataclass
|
|
52
|
-
class RouteInfo:
|
|
53
|
-
path: str
|
|
54
|
-
methods: List[str]
|
|
55
|
-
decorators: List[str]
|
|
56
|
-
lineno: int
|
|
57
|
-
filename: str
|
|
58
|
-
has_auth: bool = False
|
|
59
|
-
auth_decorators: List[str] = field(default_factory=list)
|
|
60
|
-
is_exempt: bool = False
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class AuthVisitor(ast.NodeVisitor):
|
|
64
|
-
def __init__(self, filename: str, source_lines: List[str]):
|
|
65
|
-
self.filename = filename
|
|
66
|
-
self.source_lines = source_lines
|
|
67
|
-
self.findings: List[Finding] = []
|
|
68
|
-
self.routes: List[RouteInfo] = []
|
|
69
|
-
self.imports: Dict[str, str] = {}
|
|
70
|
-
self.current_class: Optional[str] = None
|
|
71
|
-
self.blueprint_routes: List[RouteInfo] = []
|
|
72
|
-
|
|
73
|
-
def get_snippet(self, node: ast.AST, context: int = 3) -> str:
|
|
74
|
-
start = max(0, node.lineno - 1 - context)
|
|
75
|
-
end = min(len(self.source_lines),
|
|
76
|
-
getattr(node, 'end_lineno', node.lineno) + context if hasattr(node, 'end_lineno') else node.lineno + context)
|
|
77
|
-
return "\n".join(self.source_lines[start:end])
|
|
78
|
-
|
|
79
|
-
def visit_Import(self, node: ast.Import):
|
|
80
|
-
for alias in node.names:
|
|
81
|
-
self.imports[alias.asname or alias.name] = alias.name
|
|
82
|
-
self.generic_visit(node)
|
|
83
|
-
|
|
84
|
-
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
85
|
-
if node.module:
|
|
86
|
-
for alias in node.names:
|
|
87
|
-
self.imports[alias.asname or alias.name] = f"{node.module}.{alias.name}"
|
|
88
|
-
self.generic_visit(node)
|
|
89
|
-
|
|
90
|
-
def visit_ClassDef(self, node: ast.ClassDef):
|
|
91
|
-
old_class = self.current_class
|
|
92
|
-
self.current_class = node.name
|
|
93
|
-
self.generic_visit(node)
|
|
94
|
-
self.current_class = old_class
|
|
95
|
-
|
|
96
|
-
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
97
|
-
self._check_function(node)
|
|
98
|
-
self.generic_visit(node)
|
|
99
|
-
|
|
100
|
-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
101
|
-
self._check_function(node)
|
|
102
|
-
self.generic_visit(node)
|
|
103
|
-
|
|
104
|
-
def _check_function(self, node: ast.FunctionDef):
|
|
105
|
-
route_info = self._extract_route(node)
|
|
106
|
-
if route_info:
|
|
107
|
-
self._analyze_route(route_info)
|
|
108
|
-
if not route_info.is_exempt:
|
|
109
|
-
self.routes.append(route_info)
|
|
110
|
-
|
|
111
|
-
def _extract_route(self, node: ast.FunctionDef) -> Optional[RouteInfo]:
|
|
112
|
-
decorators = []
|
|
113
|
-
for dec in node.decorator_list:
|
|
114
|
-
dec_str = self._decorator_to_string(dec)
|
|
115
|
-
decorators.append(dec_str)
|
|
116
|
-
|
|
117
|
-
for dec in decorators:
|
|
118
|
-
if self._is_route_decorator(dec):
|
|
119
|
-
path, methods = self._extract_path_and_methods(dec, node)
|
|
120
|
-
if path:
|
|
121
|
-
return RouteInfo(
|
|
122
|
-
path=path,
|
|
123
|
-
methods=methods,
|
|
124
|
-
decorators=decorators,
|
|
125
|
-
lineno=node.lineno,
|
|
126
|
-
filename=self.filename,
|
|
127
|
-
)
|
|
128
|
-
return None
|
|
129
|
-
|
|
130
|
-
def _decorator_to_string(self, node: ast.AST) -> str:
|
|
131
|
-
if isinstance(node, ast.Name):
|
|
132
|
-
return node.id
|
|
133
|
-
elif isinstance(node, ast.Attribute):
|
|
134
|
-
return self._decorator_to_string(node.value) + "." + node.attr
|
|
135
|
-
elif isinstance(node, ast.Call):
|
|
136
|
-
return self._decorator_to_string(node.func)
|
|
137
|
-
return ""
|
|
138
|
-
|
|
139
|
-
def _is_route_decorator(self, dec: str) -> bool:
|
|
140
|
-
dec_lower = dec.lower()
|
|
141
|
-
for framework, patterns in ROUTE_DECORATORS.items():
|
|
142
|
-
for pattern in patterns:
|
|
143
|
-
if pattern in dec_lower:
|
|
144
|
-
return True
|
|
145
|
-
return False
|
|
146
|
-
|
|
147
|
-
def _extract_path_and_methods(self, dec: str, node: ast.FunctionDef) -> Tuple[str, List[str]]:
|
|
148
|
-
path = ""
|
|
149
|
-
methods = ["GET"]
|
|
150
|
-
|
|
151
|
-
dec_lower = dec.lower()
|
|
152
|
-
|
|
153
|
-
if any(m in dec_lower for m in [".get", "@get", "@app.get", "@router.get"]):
|
|
154
|
-
methods = ["GET"]
|
|
155
|
-
elif any(m in dec_lower for m in [".post", "@post", "@app.post", "@router.post"]):
|
|
156
|
-
methods = ["POST"]
|
|
157
|
-
elif any(m in dec_lower for m in [".put", "@put", "@app.put", "@router.put"]):
|
|
158
|
-
methods = ["PUT"]
|
|
159
|
-
elif any(m in dec_lower for m in [".delete", "@delete", "@app.delete", "@router.delete"]):
|
|
160
|
-
methods = ["DELETE"]
|
|
161
|
-
elif any(m in dec_lower for m in [".patch", "@patch", "@app.patch", "@router.patch"]):
|
|
162
|
-
methods = ["PATCH"]
|
|
163
|
-
|
|
164
|
-
if "(" in dec:
|
|
165
|
-
try:
|
|
166
|
-
call_part = dec[dec.index("(")+1:dec.rindex(")")]
|
|
167
|
-
parts = [p.strip().strip('"\'') for p in call_part.split(",")]
|
|
168
|
-
if parts:
|
|
169
|
-
path = parts[0]
|
|
170
|
-
if len(parts) > 1 and "method" in parts[1].lower():
|
|
171
|
-
pass
|
|
172
|
-
except:
|
|
173
|
-
pass
|
|
174
|
-
|
|
175
|
-
if not path:
|
|
176
|
-
path = f"/{node.name}"
|
|
177
|
-
|
|
178
|
-
return path, methods
|
|
179
|
-
|
|
180
|
-
def _analyze_route(self, route: RouteInfo):
|
|
181
|
-
route.has_auth = False
|
|
182
|
-
route.auth_decorators = []
|
|
183
|
-
|
|
184
|
-
for dec in route.decorators:
|
|
185
|
-
if self._is_auth_decorator(dec):
|
|
186
|
-
route.has_auth = True
|
|
187
|
-
route.auth_decorators.append(dec)
|
|
188
|
-
|
|
189
|
-
if not route.has_auth:
|
|
190
|
-
if self.current_class:
|
|
191
|
-
for base in getattr(self, 'class_bases', []):
|
|
192
|
-
if self._is_auth_mixin(base):
|
|
193
|
-
route.has_auth = True
|
|
194
|
-
route.auth_decorators.append(f"Mixin:{base}")
|
|
195
|
-
|
|
196
|
-
route.is_exempt = self._is_exempt_path(route.path)
|
|
197
|
-
|
|
198
|
-
def _is_auth_decorator(self, dec: str) -> bool:
|
|
199
|
-
dec_lower = dec.lower()
|
|
200
|
-
for framework, patterns in AUTH_DECORATORS.items():
|
|
201
|
-
for pattern in patterns:
|
|
202
|
-
if pattern.lower() in dec_lower:
|
|
203
|
-
return True
|
|
204
|
-
return False
|
|
205
|
-
|
|
206
|
-
def _is_auth_mixin(self, base: str) -> bool:
|
|
207
|
-
base_lower = base.lower()
|
|
208
|
-
auth_mixins = ["loginrequiredmixin", "permissionrequiredmixin", "userpassestestmixin",
|
|
209
|
-
"staffmemberrequiredmixin", "superuserrequiredmixin",
|
|
210
|
-
"accesscontrolmixin", "authorizationmixin"]
|
|
211
|
-
return any(m in base_lower for m in auth_mixins)
|
|
212
|
-
|
|
213
|
-
def _is_exempt_path(self, path: str) -> bool:
|
|
214
|
-
for pattern in EXEMPT_PATTERNS:
|
|
215
|
-
if re.search(pattern, path, re.IGNORECASE):
|
|
216
|
-
return True
|
|
217
|
-
return False
|
|
218
|
-
|
|
219
|
-
def visit_Call(self, node: ast.Call):
|
|
220
|
-
if isinstance(node.func, ast.Attribute):
|
|
221
|
-
if node.func.attr in ("add_url_rule", "route"):
|
|
222
|
-
if node.args:
|
|
223
|
-
path = self._get_string_value(node.args[0])
|
|
224
|
-
if path:
|
|
225
|
-
methods = ["GET"]
|
|
226
|
-
if len(node.args) > 1:
|
|
227
|
-
methods_arg = node.args[1]
|
|
228
|
-
if isinstance(methods_arg, ast.List):
|
|
229
|
-
methods = [self._get_string_value(e) or "GET" for e in methods_arg.elts]
|
|
230
|
-
|
|
231
|
-
route = RouteInfo(
|
|
232
|
-
path=path,
|
|
233
|
-
methods=methods,
|
|
234
|
-
decorators=[f"add_url_rule({path})"],
|
|
235
|
-
lineno=node.lineno,
|
|
236
|
-
filename=self.filename,
|
|
237
|
-
)
|
|
238
|
-
self._analyze_route(route)
|
|
239
|
-
if not route.is_exempt:
|
|
240
|
-
self.routes.append(route)
|
|
241
|
-
self.generic_visit(node)
|
|
242
|
-
|
|
243
|
-
def _get_string_value(self, node: ast.AST) -> Optional[str]:
|
|
244
|
-
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
245
|
-
return node.value
|
|
246
|
-
if isinstance(node, ast.Str):
|
|
247
|
-
return node.s
|
|
248
|
-
return None
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
class Engine:
|
|
252
|
-
name = "authorization"
|
|
253
|
-
version = "1.0.0"
|
|
254
|
-
|
|
255
|
-
async def check_availability(self) -> bool:
|
|
256
|
-
return True
|
|
257
|
-
|
|
258
|
-
def resource_limits(self) -> ResourceLimits:
|
|
259
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
|
|
260
|
-
|
|
261
|
-
async def run(self, ctx, config: EngineConfig) -> EngineResult:
|
|
262
|
-
# Handle both single IR and list of IRs
|
|
263
|
-
if not isinstance(ctx, list):
|
|
264
|
-
ctx = [ctx]
|
|
265
|
-
all_findings = []
|
|
266
|
-
errors = []
|
|
267
|
-
files_processed = 0
|
|
268
|
-
total_routes = 0
|
|
269
|
-
unprotected_routes = 0
|
|
270
|
-
|
|
271
|
-
exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
|
|
272
|
-
|
|
273
|
-
for ir in ctx:
|
|
274
|
-
try:
|
|
275
|
-
if not ir.metadata.get("language") == "python":
|
|
276
|
-
continue
|
|
277
|
-
|
|
278
|
-
rel_path = ir.metadata.get("path", "unknown.py")
|
|
279
|
-
if any(pattern in rel_path for pattern in exclude_patterns):
|
|
280
|
-
continue
|
|
281
|
-
|
|
282
|
-
content = ir.content.decode("utf-8", errors="replace")
|
|
283
|
-
source_lines = content.splitlines()
|
|
284
|
-
tree = ast.parse(content, filename=rel_path)
|
|
285
|
-
|
|
286
|
-
visitor = AuthVisitor(rel_path, source_lines)
|
|
287
|
-
visitor.visit(tree)
|
|
288
|
-
|
|
289
|
-
for route in visitor.routes:
|
|
290
|
-
total_routes += 1
|
|
291
|
-
if not route.has_auth:
|
|
292
|
-
unprotected_routes += 1
|
|
293
|
-
severity = "high" if any(m in route.methods for m in ["POST", "PUT", "DELETE", "PATCH"]) else "medium"
|
|
294
|
-
|
|
295
|
-
all_findings.append(Finding(
|
|
296
|
-
engine="authorization",
|
|
297
|
-
severity=severity,
|
|
298
|
-
title="Missing authentication on route",
|
|
299
|
-
description=f"Route '{route.path}' [{', '.join(route.methods)}] has no authentication decorator. "
|
|
300
|
-
f"Consider adding @login_required, @auth_required, or Depends(get_current_user).",
|
|
301
|
-
location=f"{route.filename}:{route.lineno}",
|
|
302
|
-
metadata={
|
|
303
|
-
"snippet": self.get_snippet_by_line(source_lines, route.lineno),
|
|
304
|
-
"cwe": "CWE-306",
|
|
305
|
-
"confidence": 0.85,
|
|
306
|
-
"route_path": route.path,
|
|
307
|
-
"route_methods": route.methods,
|
|
308
|
-
"decorators": route.decorators,
|
|
309
|
-
"is_exempt": route.is_exempt,
|
|
310
|
-
}
|
|
311
|
-
))
|
|
312
|
-
|
|
313
|
-
files_processed += 1
|
|
314
|
-
|
|
315
|
-
except SyntaxError as e:
|
|
316
|
-
errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
|
|
317
|
-
except Exception as e:
|
|
318
|
-
errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
|
|
319
|
-
|
|
320
|
-
return EngineResult(
|
|
321
|
-
engine=self.name,
|
|
322
|
-
findings=all_findings,
|
|
323
|
-
errors=errors,
|
|
324
|
-
metrics={
|
|
325
|
-
"files_processed": files_processed,
|
|
326
|
-
"total_routes": total_routes,
|
|
327
|
-
"unprotected_routes": unprotected_routes,
|
|
328
|
-
"protected_routes": total_routes - unprotected_routes,
|
|
329
|
-
"findings_count": len(all_findings),
|
|
330
|
-
},
|
|
331
|
-
ok=len(errors) == 0
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
def get_snippet_by_line(self, lines: List[str], line: int, context: int = 3) -> str:
|
|
335
|
-
start = max(0, line - 1 - context)
|
|
336
|
-
end = min(len(lines), line + context)
|
|
337
|
-
return "\n".join(lines[start:end])
|
|
Binary file
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
"""Function call graph builder engine."""
|
|
2
|
-
import ast
|
|
3
|
-
import asyncio
|
|
4
|
-
from collections import defaultdict
|
|
5
|
-
from typing import Dict, List, Set, Optional, Any
|
|
6
|
-
from orion.engine.config import EngineConfig, ResourceLimits
|
|
7
|
-
from orion.engine.ir import OrionIR
|
|
8
|
-
from orion.engine.results import EngineResult, Finding
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class CallGraphVisitor(ast.NodeVisitor):
|
|
12
|
-
def __init__(self, filename: str, source_lines: List[str]):
|
|
13
|
-
self.filename = filename
|
|
14
|
-
self.source_lines = source_lines
|
|
15
|
-
self.calls: Dict[str, Set[str]] = defaultdict(set)
|
|
16
|
-
self.function_defs: Set[str] = set()
|
|
17
|
-
self.class_defs: Set[str] = set()
|
|
18
|
-
self.imports: Dict[str, str] = {}
|
|
19
|
-
self.current_class: Optional[str] = None
|
|
20
|
-
self.current_function: Optional[str] = None
|
|
21
|
-
self.call_depth = 0
|
|
22
|
-
self.max_call_depth = 100
|
|
23
|
-
self.undefined_calls: List[tuple] = []
|
|
24
|
-
self.recursive_calls: List[tuple] = []
|
|
25
|
-
self.deep_chains: List[tuple] = []
|
|
26
|
-
|
|
27
|
-
def get_qualified_name(self, name: str) -> str:
|
|
28
|
-
parts = []
|
|
29
|
-
if self.current_class:
|
|
30
|
-
parts.append(self.current_class)
|
|
31
|
-
if self.current_function:
|
|
32
|
-
parts.append(self.current_function)
|
|
33
|
-
parts.append(name)
|
|
34
|
-
return ".".join(parts)
|
|
35
|
-
|
|
36
|
-
def visit_Import(self, node: ast.Import):
|
|
37
|
-
for alias in node.names:
|
|
38
|
-
self.imports[alias.asname or alias.name] = alias.name
|
|
39
|
-
self.generic_visit(node)
|
|
40
|
-
|
|
41
|
-
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
42
|
-
if node.module:
|
|
43
|
-
for alias in node.names:
|
|
44
|
-
self.imports[alias.asname or alias.name] = f"{node.module}.{alias.name}"
|
|
45
|
-
self.generic_visit(node)
|
|
46
|
-
|
|
47
|
-
def visit_ClassDef(self, node: ast.ClassDef):
|
|
48
|
-
old_class = self.current_class
|
|
49
|
-
self.current_class = node.name
|
|
50
|
-
self.class_defs.add(self.get_qualified_name(node.name))
|
|
51
|
-
self.generic_visit(node)
|
|
52
|
-
self.current_class = old_class
|
|
53
|
-
|
|
54
|
-
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
55
|
-
old_function = self.current_function
|
|
56
|
-
self.current_function = node.name
|
|
57
|
-
qual_name = self.get_qualified_name(node.name)
|
|
58
|
-
self.function_defs.add(qual_name)
|
|
59
|
-
self.generic_visit(node)
|
|
60
|
-
self.current_function = old_function
|
|
61
|
-
|
|
62
|
-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
63
|
-
self.visit_FunctionDef(node)
|
|
64
|
-
|
|
65
|
-
def visit_Call(self, node: ast.Call):
|
|
66
|
-
if self.call_depth > self.max_call_depth:
|
|
67
|
-
return
|
|
68
|
-
self.call_depth += 1
|
|
69
|
-
try:
|
|
70
|
-
caller = self.current_function or "<module>"
|
|
71
|
-
if self.current_class:
|
|
72
|
-
caller = f"{self.current_class}.{caller}"
|
|
73
|
-
|
|
74
|
-
callee = self._resolve_callee(node.func)
|
|
75
|
-
if callee:
|
|
76
|
-
self.calls[caller].add(callee)
|
|
77
|
-
|
|
78
|
-
if callee == caller:
|
|
79
|
-
self.recursive_calls.append((caller, node.lineno))
|
|
80
|
-
|
|
81
|
-
if callee not in self.function_defs and not self._is_builtin(callee):
|
|
82
|
-
self.undefined_calls.append((caller, callee, node.lineno))
|
|
83
|
-
|
|
84
|
-
self.generic_visit(node)
|
|
85
|
-
finally:
|
|
86
|
-
self.call_depth -= 1
|
|
87
|
-
|
|
88
|
-
def _resolve_callee(self, node: ast.AST) -> Optional[str]:
|
|
89
|
-
if isinstance(node, ast.Name):
|
|
90
|
-
return node.id
|
|
91
|
-
elif isinstance(node, ast.Attribute):
|
|
92
|
-
parts = []
|
|
93
|
-
current = node
|
|
94
|
-
while isinstance(current, ast.Attribute):
|
|
95
|
-
parts.append(current.attr)
|
|
96
|
-
current = current.value
|
|
97
|
-
if isinstance(current, ast.Name):
|
|
98
|
-
parts.append(current.id)
|
|
99
|
-
return ".".join(reversed(parts))
|
|
100
|
-
elif isinstance(current, ast.Call):
|
|
101
|
-
return "<dynamic>"
|
|
102
|
-
return ".".join(reversed(parts))
|
|
103
|
-
elif isinstance(node, ast.Call):
|
|
104
|
-
return "<dynamic>"
|
|
105
|
-
elif isinstance(node, ast.Subscript):
|
|
106
|
-
return "<subscript>"
|
|
107
|
-
elif isinstance(node, ast.Lambda):
|
|
108
|
-
return "<lambda>"
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
def _is_builtin(self, name: str) -> bool:
|
|
112
|
-
builtins = {
|
|
113
|
-
"print", "len", "str", "int", "float", "list", "dict", "set", "tuple",
|
|
114
|
-
"range", "enumerate", "zip", "map", "filter", "sorted", "reversed",
|
|
115
|
-
"sum", "min", "max", "abs", "round", "pow", "divmod", "isinstance",
|
|
116
|
-
"issubclass", "hasattr", "getattr", "setattr", "delattr", "callable",
|
|
117
|
-
"type", "object", "super", "property", "staticmethod", "classmethod",
|
|
118
|
-
"open", "input", "eval", "exec", "compile", "hash", "id", "hex", "oct",
|
|
119
|
-
"bin", "chr", "ord", "ascii", "repr", "format", "vars", "locals", "globals",
|
|
120
|
-
"dir", "help", "breakpoint", "__import__", "iter", "next", "slice",
|
|
121
|
-
"property", "complex", "bool", "bytes", "bytearray", "memoryview",
|
|
122
|
-
"frozenset", "classmethod", "staticmethod", "property",
|
|
123
|
-
"Exception", "BaseException", "ValueError", "TypeError", "KeyError",
|
|
124
|
-
"IndexError", "AttributeError", "ImportError", "ModuleNotFoundError",
|
|
125
|
-
"OSError", "IOError", "RuntimeError", "StopIteration", "GeneratorExit",
|
|
126
|
-
"KeyboardInterrupt", "SystemExit", "NameError", "SyntaxError",
|
|
127
|
-
}
|
|
128
|
-
return name in builtins or name.startswith("__") and name.endswith("__")
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
class Engine:
|
|
132
|
-
name = "callgraph"
|
|
133
|
-
version = "1.0.0"
|
|
134
|
-
|
|
135
|
-
async def check_availability(self) -> bool:
|
|
136
|
-
return True
|
|
137
|
-
|
|
138
|
-
def resource_limits(self) -> ResourceLimits:
|
|
139
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
|
|
140
|
-
|
|
141
|
-
async def run(self, ctx, config: EngineConfig) -> EngineResult:
|
|
142
|
-
# Handle both single IR and list of IRs
|
|
143
|
-
if not isinstance(ctx, list):
|
|
144
|
-
ctx = [ctx]
|
|
145
|
-
all_calls: Dict[str, Set[str]] = defaultdict(set)
|
|
146
|
-
all_functions: Set[str] = set()
|
|
147
|
-
all_classes: Set[str] = set()
|
|
148
|
-
all_undefined: List[tuple] = []
|
|
149
|
-
all_recursive: List[tuple] = []
|
|
150
|
-
errors = []
|
|
151
|
-
files_processed = 0
|
|
152
|
-
|
|
153
|
-
exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
|
|
154
|
-
|
|
155
|
-
for ir in ctx:
|
|
156
|
-
try:
|
|
157
|
-
if not ir.metadata.get("language") == "python":
|
|
158
|
-
continue
|
|
159
|
-
|
|
160
|
-
rel_path = ir.metadata.get("path", "unknown.py")
|
|
161
|
-
if any(pattern in rel_path for pattern in exclude_patterns):
|
|
162
|
-
continue
|
|
163
|
-
|
|
164
|
-
content = ir.content.decode("utf-8", errors="replace")
|
|
165
|
-
source_lines = content.splitlines()
|
|
166
|
-
tree = ast.parse(content, filename=rel_path)
|
|
167
|
-
|
|
168
|
-
visitor = CallGraphVisitor(rel_path, source_lines)
|
|
169
|
-
visitor.visit(tree)
|
|
170
|
-
|
|
171
|
-
for caller, callees in visitor.calls.items():
|
|
172
|
-
all_calls[caller].update(callees)
|
|
173
|
-
all_functions.update(visitor.function_defs)
|
|
174
|
-
all_classes.update(visitor.class_defs)
|
|
175
|
-
all_undefined.extend(visitor.undefined_calls)
|
|
176
|
-
all_recursive.extend(visitor.recursive_calls)
|
|
177
|
-
files_processed += 1
|
|
178
|
-
|
|
179
|
-
except SyntaxError as e:
|
|
180
|
-
errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
|
|
181
|
-
except Exception as e:
|
|
182
|
-
errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
|
|
183
|
-
|
|
184
|
-
deep_chains = self._find_deep_chains(all_calls)
|
|
185
|
-
|
|
186
|
-
call_graph = {k: list(v) for k, v in all_calls.items()}
|
|
187
|
-
|
|
188
|
-
metrics = {
|
|
189
|
-
"files_processed": files_processed,
|
|
190
|
-
"functions_defined": len(all_functions),
|
|
191
|
-
"classes_defined": len(all_classes),
|
|
192
|
-
"total_edges": sum(len(v) for v in all_calls.values()),
|
|
193
|
-
"unique_callers": len(all_calls),
|
|
194
|
-
"undefined_calls_count": len(all_undefined),
|
|
195
|
-
"recursive_calls_count": len(all_recursive),
|
|
196
|
-
"deep_chains_count": len(deep_chains),
|
|
197
|
-
"call_graph": call_graph,
|
|
198
|
-
"undefined_calls": all_undefined[:50],
|
|
199
|
-
"recursive_calls": all_recursive[:50],
|
|
200
|
-
"deep_chains": deep_chains[:20],
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return EngineResult(
|
|
204
|
-
engine=self.name,
|
|
205
|
-
findings=[],
|
|
206
|
-
errors=errors,
|
|
207
|
-
metrics=metrics,
|
|
208
|
-
ok=len(errors) == 0
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
def _find_deep_chains(self, calls: Dict[str, Set[str]], max_depth: int = 10) -> List[tuple]:
|
|
212
|
-
chains = []
|
|
213
|
-
visited = set()
|
|
214
|
-
|
|
215
|
-
def dfs(node: str, path: List[str], depth: int):
|
|
216
|
-
if depth > max_depth:
|
|
217
|
-
chains.append((path[0], path[-1], depth, path.copy()))
|
|
218
|
-
return
|
|
219
|
-
if node in visited:
|
|
220
|
-
return
|
|
221
|
-
if node in path:
|
|
222
|
-
return
|
|
223
|
-
|
|
224
|
-
visited.add(node)
|
|
225
|
-
path.append(node)
|
|
226
|
-
|
|
227
|
-
for callee in calls.get(node, []):
|
|
228
|
-
dfs(callee, path, depth + 1)
|
|
229
|
-
|
|
230
|
-
path.pop()
|
|
231
|
-
visited.discard(node)
|
|
232
|
-
|
|
233
|
-
for caller in calls:
|
|
234
|
-
dfs(caller, [], 0)
|
|
235
|
-
|
|
236
|
-
return chains
|
|
Binary file
|