@orionscanner/cli 0.1.0

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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +248 -0
  3. package/assets/logo.png +0 -0
  4. package/bin/orion.js +48 -0
  5. package/docs/API_REFERENCE.md +211 -0
  6. package/docs/ARCHITECTURE.md +183 -0
  7. package/docs/CHANGELOG.md +112 -0
  8. package/docs/CONTRIBUTING.md +157 -0
  9. package/docs/ENGINE_CATALOG.md +87 -0
  10. package/docs/GETTING_STARTED.md +226 -0
  11. package/package.json +48 -0
  12. package/pyproject.toml +16 -0
  13. package/src/orion/__init__.py +0 -0
  14. package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/src/orion/engine/__init__.py +17 -0
  16. package/src/orion/engine/__pycache__/__init__.cpython-313.pyc +0 -0
  17. package/src/orion/engine/__pycache__/config.cpython-313.pyc +0 -0
  18. package/src/orion/engine/__pycache__/core.cpython-313.pyc +0 -0
  19. package/src/orion/engine/__pycache__/ir.cpython-313.pyc +0 -0
  20. package/src/orion/engine/__pycache__/languages.cpython-313.pyc +0 -0
  21. package/src/orion/engine/__pycache__/registry.cpython-313.pyc +0 -0
  22. package/src/orion/engine/__pycache__/results.cpython-313.pyc +0 -0
  23. package/src/orion/engine/__pycache__/runner.cpython-313.pyc +0 -0
  24. package/src/orion/engine/api_security/__pycache__/engine.cpython-313.pyc +0 -0
  25. package/src/orion/engine/api_security/engine.py +125 -0
  26. package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
  27. package/src/orion/engine/architecture/engine.py +463 -0
  28. package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
  29. package/src/orion/engine/authorization/engine.py +337 -0
  30. package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
  31. package/src/orion/engine/callgraph/engine.py +236 -0
  32. package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
  33. package/src/orion/engine/cfg/engine.py +509 -0
  34. package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
  35. package/src/orion/engine/chaos/engine.py +268 -0
  36. package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
  37. package/src/orion/engine/chaos_config/engine.py +322 -0
  38. package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
  39. package/src/orion/engine/complexity/engine.py +144 -0
  40. package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
  41. package/src/orion/engine/compliance/engine.py +325 -0
  42. package/src/orion/engine/config.py +41 -0
  43. package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
  44. package/src/orion/engine/config_engine/engine.py +364 -0
  45. package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
  46. package/src/orion/engine/config_validation/engine.py +313 -0
  47. package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
  48. package/src/orion/engine/container/engine.py +546 -0
  49. package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
  50. package/src/orion/engine/contract_probe/engine.py +316 -0
  51. package/src/orion/engine/core.py +75 -0
  52. package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
  53. package/src/orion/engine/crypto_guard/engine.py +134 -0
  54. package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
  55. package/src/orion/engine/data_residency/engine.py +238 -0
  56. package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
  57. package/src/orion/engine/dataflow/engine.py +398 -0
  58. package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
  59. package/src/orion/engine/dead_import/engine.py +293 -0
  60. package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
  61. package/src/orion/engine/deps/engine.py +244 -0
  62. package/src/orion/engine/dispatcher.py +350 -0
  63. package/src/orion/engine/docs/AUDIT_PROMPT.md +1248 -0
  64. package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
  65. package/src/orion/engine/duplication/engine.py +120 -0
  66. package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
  67. package/src/orion/engine/error_handling/engine.py +256 -0
  68. package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
  69. package/src/orion/engine/fp_learn/engine.py +248 -0
  70. package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
  71. package/src/orion/engine/hunter/engine.py +324 -0
  72. package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
  73. package/src/orion/engine/iac_security/engine.py +218 -0
  74. package/src/orion/engine/ir.py +22 -0
  75. package/src/orion/engine/languages.py +454 -0
  76. package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
  77. package/src/orion/engine/license/engine.py +172 -0
  78. package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
  79. package/src/orion/engine/lint/engine.py +558 -0
  80. package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
  81. package/src/orion/engine/llm_security/engine.py +125 -0
  82. package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
  83. package/src/orion/engine/logging_c/engine.py +164 -0
  84. package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
  85. package/src/orion/engine/noise/engine.py +140 -0
  86. package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
  87. package/src/orion/engine/orphan/engine.py +244 -0
  88. package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
  89. package/src/orion/engine/perf_guard/engine.py +345 -0
  90. package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
  91. package/src/orion/engine/prompt_security/detectors/__init__.py +4 -0
  92. package/src/orion/engine/prompt_security/engine.py +170 -0
  93. package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
  94. package/src/orion/engine/provenance/engine.py +242 -0
  95. package/src/orion/engine/registry.py +55 -0
  96. package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
  97. package/src/orion/engine/regression/engine.py +213 -0
  98. package/src/orion/engine/results.py +21 -0
  99. package/src/orion/engine/runner.py +1310 -0
  100. package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
  101. package/src/orion/engine/sandbox_escape/engine.py +284 -0
  102. package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
  103. package/src/orion/engine/sast/engine.py +217 -0
  104. package/src/orion/engine/sast/patterns/__init__.py +513 -0
  105. package/src/orion/engine/sast/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  106. package/src/orion/engine/secrets/__pycache__/engine.cpython-313.pyc +0 -0
  107. package/src/orion/engine/secrets/engine.py +77 -0
  108. package/src/orion/engine/secrets/patterns/__init__.py +74 -0
  109. package/src/orion/engine/secrets/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  110. package/src/orion/engine/sensitive_data/__pycache__/engine.cpython-313.pyc +0 -0
  111. package/src/orion/engine/sensitive_data/engine.py +143 -0
  112. package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
  113. package/src/orion/engine/state/engine.py +218 -0
  114. package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
  115. package/src/orion/engine/supply_chain_sbom/engine.py +290 -0
  116. package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
  117. package/src/orion/engine/symbol/engine.py +463 -0
  118. package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
  119. package/src/orion/engine/taint/engine.py +464 -0
  120. package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
  121. package/src/orion/engine/test_coverage/engine.py +445 -0
  122. package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
  123. package/src/orion/engine/threat_model/engine.py +126 -0
  124. package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
  125. package/src/orion/engine/wiring_trace/engine.py +200 -0
  126. package/src/orion_engine.egg-info/PKG-INFO +7 -0
  127. package/src/orion_engine.egg-info/SOURCES.txt +65 -0
  128. package/src/orion_engine.egg-info/dependency_links.txt +1 -0
  129. package/src/orion_engine.egg-info/entry_points.txt +2 -0
  130. package/src/orion_engine.egg-info/top_level.txt +1 -0
@@ -0,0 +1,345 @@
1
+ """Performance anti-patterns detection engine - AST analysis."""
2
+ import ast
3
+ import re
4
+ from orion.engine.config import EngineConfig
5
+ from orion.engine.ir import OrionIR
6
+ from orion.engine.results import EngineResult, Finding
7
+
8
+
9
+ class Engine:
10
+ name = "perf_guard"
11
+ version = "1.0.0"
12
+
13
+ DB_QUERY_METHODS = {
14
+ "execute", "executemany", "query", "find", "find_one", "get", "filter",
15
+ "filter_by", "all", "first", "one", "scalar", "scalars", "fetchone",
16
+ "fetchall", "fetchmany", "select", "insert", "update", "delete",
17
+ "aggregate", "count", "exists", "bulk_insert", "bulk_update",
18
+ }
19
+
20
+ HOT_LOOP_METHODS = {"append", "extend", "insert", "add", "update"}
21
+ STRING_CONCAT_PATTERNS = {"__iadd__", "__add__"}
22
+ SYNC_IO_IN_ASYNC = {"open", "read", "write", "readline", "readlines", "requests.get", "requests.post", "requests.put", "requests.delete", "requests.request", "urllib.request.urlopen", "httpx.get", "httpx.post", "httpx.put", "httpx.delete"}
23
+
24
+ COMPILE_PATTERN = re.compile(r"re\.compile|re\.search|re\.match|re\.findall|re\.sub|re\.split")
25
+
26
+ async def check_availability(self) -> bool:
27
+ return True
28
+
29
+ def resource_limits(self):
30
+ from orion.engine.config import ResourceLimits
31
+ return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
32
+
33
+ async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
34
+ try:
35
+ text = ir.content.decode("utf-8", errors="replace")
36
+ except Exception:
37
+ return EngineResult(engine=self.name, findings=[], ok=True)
38
+
39
+ path = ir.metadata.get("path", "unknown")
40
+ is_test = self._is_test_file(path)
41
+
42
+ try:
43
+ tree = ast.parse(text, filename=path)
44
+ except SyntaxError:
45
+ return EngineResult(engine=self.name, findings=[], ok=True)
46
+
47
+ findings = []
48
+ visitor = PerfVisitor(path, is_test, text.splitlines())
49
+ visitor.visit(tree)
50
+ findings.extend(visitor.findings)
51
+
52
+ findings.extend(self._check_regex_compile_in_loop(text, path))
53
+ findings.extend(self._check_json_in_hot_path(text, path))
54
+
55
+ return EngineResult(engine=self.name, findings=findings, ok=True)
56
+
57
+ def _is_test_file(self, path: str) -> bool:
58
+ return any(p in path for p in ["test_", "_test.py", "/tests/", "/test/", "conftest.py"])
59
+
60
+ def _check_regex_compile_in_loop(self, text: str, path: str) -> list[Finding]:
61
+ findings = []
62
+ lines = text.splitlines()
63
+ in_loop = False
64
+ loop_depth = 0
65
+
66
+ for i, line in enumerate(lines):
67
+ stripped = line.strip()
68
+ if re.match(r"^\s*(for|while)\s+", stripped):
69
+ loop_depth += 1
70
+ in_loop = True
71
+ elif stripped and not stripped.startswith("#"):
72
+ if loop_depth > 0:
73
+ if self.COMPILE_PATTERN.search(line):
74
+ findings.append(Finding(
75
+ engine=self.name,
76
+ severity="medium",
77
+ title="re.compile in loop",
78
+ description="Regex compilation inside loop - compile once outside loop",
79
+ location=f"{path}:{i + 1}",
80
+ metadata={
81
+ "confidence": 0.85,
82
+ "snippet": line.strip()[:200],
83
+ "pattern": "re_compile_in_loop",
84
+ "cwe": "CWE-1054",
85
+ },
86
+ ))
87
+
88
+ if loop_depth > 0 and stripped and not re.match(r"^\s*(for|while|if|elif|else|try|except|finally|with|def|class|async)\s", stripped):
89
+ pass
90
+ if "dedent" in stripped.lower() or (stripped == "" and loop_depth > 0):
91
+ pass
92
+
93
+ indent = len(line) - len(line.lstrip())
94
+ if i + 1 < len(lines):
95
+ next_indent = len(lines[i + 1]) - len(lines[i + 1].lstrip())
96
+ if next_indent < indent and loop_depth > 0:
97
+ loop_depth = max(0, loop_depth - 1)
98
+
99
+ return findings
100
+
101
+ def _check_json_in_hot_path(self, text: str, path: str) -> list[Finding]:
102
+ findings = []
103
+ lines = text.splitlines()
104
+ in_loop = False
105
+ loop_depth = 0
106
+
107
+ for i, line in enumerate(lines):
108
+ stripped = line.strip()
109
+ if re.match(r"^\s*(for|while)\s+", stripped):
110
+ loop_depth += 1
111
+ in_loop = True
112
+ elif in_loop and ("json.loads" in line or "json.dumps" in line or "json.load" in line or "json.dump" in line):
113
+ findings.append(Finding(
114
+ engine=self.name,
115
+ severity="medium",
116
+ title="json.loads/dumps in loop",
117
+ description="JSON serialization in hot loop - consider batching or caching",
118
+ location=f"{path}:{i + 1}",
119
+ metadata={
120
+ "confidence": 0.8,
121
+ "snippet": line.strip()[:200],
122
+ "pattern": "json_in_loop",
123
+ "cwe": "CWE-1054",
124
+ },
125
+ ))
126
+
127
+ indent = len(line) - len(line.lstrip())
128
+ if i + 1 < len(lines):
129
+ next_indent = len(lines[i + 1]) - len(lines[i + 1].lstrip())
130
+ if next_indent < indent and loop_depth > 0:
131
+ loop_depth = max(0, loop_depth - 1)
132
+
133
+ return findings
134
+
135
+
136
+ class PerfVisitor(ast.NodeVisitor):
137
+ def __init__(self, filepath: str, is_test: bool, lines: list[str]):
138
+ self.filepath = filepath
139
+ self.is_test = is_test
140
+ self.lines = lines
141
+ self.findings = []
142
+ self.loop_vars = []
143
+ self.in_async_function = False
144
+ self.current_loop_depth = 0
145
+
146
+ def visit_For(self, node: ast.For):
147
+ old_depth = self.current_loop_depth
148
+ self.current_loop_depth += 1
149
+
150
+ if self.current_loop_depth >= 2:
151
+ self._check_nested_loops(node)
152
+
153
+ self._check_db_query_in_loop(node)
154
+ self._check_list_append_in_loop(node)
155
+ self._check_string_concat_in_loop(node)
156
+ self._check_large_in_memory_load(node)
157
+ self._check_missing_pagination(node)
158
+
159
+ old_vars = self.loop_vars[:]
160
+ if isinstance(node.target, ast.Name):
161
+ self.loop_vars.append(node.target.id)
162
+ elif isinstance(node.target, ast.Tuple):
163
+ for elt in node.target.elts:
164
+ if isinstance(elt, ast.Name):
165
+ self.loop_vars.append(elt.id)
166
+
167
+ self.generic_visit(node)
168
+
169
+ self.loop_vars = old_vars
170
+ self.current_loop_depth = old_depth
171
+
172
+ def visit_While(self, node: ast.While):
173
+ old_depth = self.current_loop_depth
174
+ self.current_loop_depth += 1
175
+
176
+ if self.current_loop_depth >= 2:
177
+ self._check_nested_loops(node)
178
+
179
+ self._check_db_query_in_loop(node)
180
+ self._check_list_append_in_loop(node)
181
+ self._check_string_concat_in_loop(node)
182
+ self._check_large_in_memory_load(node)
183
+ self._check_missing_pagination(node)
184
+
185
+ # While nodes don't have a target like For nodes
186
+ old_vars = self.loop_vars[:]
187
+ self.generic_visit(node)
188
+ self.loop_vars = old_vars
189
+ self.current_loop_depth = old_depth
190
+
191
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
192
+ old_async = self.in_async_function
193
+ self.in_async_function = True
194
+ self.generic_visit(node)
195
+ self.in_async_function = old_async
196
+
197
+ def visit_Call(self, node: ast.Call):
198
+ if self.in_async_function:
199
+ self._check_sync_io_in_async(node)
200
+
201
+ self._check_repeated_compile(node)
202
+
203
+ self.generic_visit(node)
204
+
205
+ def _check_nested_loops(self, node: ast.For):
206
+ outer_var = self.loop_vars[-1] if self.loop_vars else None
207
+ inner_var = None
208
+ if isinstance(node.target, ast.Name):
209
+ inner_var = node.target.id
210
+ elif isinstance(node.target, ast.Tuple):
211
+ for elt in node.target.elts:
212
+ if isinstance(elt, ast.Name):
213
+ inner_var = elt.id
214
+ break
215
+
216
+ if outer_var and inner_var and outer_var == inner_var:
217
+ self._add_finding(node, "nested_loop_same_var",
218
+ f"Nested loops using same variable '{outer_var}' - likely O(n²)",
219
+ "Use different variable names or consider algorithm optimization",
220
+ confidence=0.8)
221
+
222
+ def _check_db_query_in_loop(self, node: ast.For):
223
+ db_callers = {"db", "session", "cursor", "conn", "connection", "repo", "repository", "query", "orm"}
224
+ strict_db_methods = {"execute", "executemany", "raw", "raw_sql", "fetchmany", "fetchall"}
225
+
226
+ for stmt in ast.walk(node):
227
+ if isinstance(stmt, ast.Call) and isinstance(stmt.func, ast.Attribute):
228
+ caller_name = ""
229
+ if isinstance(stmt.func.value, ast.Name):
230
+ caller_name = stmt.func.value.id.lower()
231
+
232
+ method_name = stmt.func.attr.lower()
233
+
234
+ # Flag only if explicit DB caller or strictly SQL execution method on a database object
235
+ if (caller_name in db_callers and method_name in {"execute", "query", "find", "select", "insert", "update", "delete"}) or \
236
+ (method_name in strict_db_methods and caller_name not in {"dict", "json", "re", "os", "sys"}):
237
+ self._add_finding(stmt, "n_plus_one_query",
238
+ f"Database query '{stmt.func.attr}' in loop - potential N+1",
239
+ "Use batch loading, joins, or prefetch to avoid N+1 queries",
240
+ confidence=0.85)
241
+ break
242
+
243
+ def _check_list_append_in_loop(self, node: ast.For):
244
+ # Do not flag standard list accumulation as an error
245
+ pass
246
+
247
+ def _is_list_comprehension_possible(self, loop_node: ast.For, list_var: str) -> bool:
248
+ append_count = 0
249
+ other_stmts = 0
250
+ for stmt in ast.walk(loop_node):
251
+ if stmt is loop_node:
252
+ continue
253
+ if isinstance(stmt, ast.Call) and isinstance(stmt.func, ast.Attribute):
254
+ if stmt.func.attr == "append" and isinstance(stmt.func.value, ast.Name) and stmt.func.value.id == list_var:
255
+ append_count += 1
256
+ elif isinstance(stmt, (ast.Assign, ast.AugAssign, ast.If, ast.For, ast.While, ast.Try, ast.With)):
257
+ other_stmts += 1
258
+ return append_count > 0 and other_stmts == 0
259
+
260
+ def _check_string_concat_in_loop(self, node: ast.For):
261
+ for stmt in ast.walk(node):
262
+ if isinstance(stmt, ast.AugAssign) and isinstance(stmt.op, ast.Add):
263
+ if isinstance(stmt.target, ast.Name):
264
+ self._add_finding(stmt, "string_concat_in_loop",
265
+ f"String concatenation (+=) in loop for '{stmt.target.id}'",
266
+ "Use list append + ''.join() or StringIO for better performance",
267
+ confidence=0.85)
268
+ break
269
+
270
+ def _check_large_in_memory_load(self, node: ast.For):
271
+ for stmt in ast.walk(node):
272
+ if isinstance(stmt, ast.Call) and isinstance(stmt.func, ast.Attribute):
273
+ if stmt.func.attr in ("fetchall", "read", "readlines", "read_all"):
274
+ if isinstance(stmt.func.value, ast.Name):
275
+ self._add_finding(stmt, "large_in_memory_load",
276
+ f"Large in-memory load '{stmt.func.attr}()' in loop",
277
+ "Use pagination, streaming, or chunked processing",
278
+ confidence=0.8)
279
+ break
280
+
281
+ def _check_missing_pagination(self, node: ast.For):
282
+ for stmt in ast.walk(node):
283
+ if isinstance(stmt, ast.Call) and isinstance(stmt.func, ast.Attribute):
284
+ if stmt.func.attr in ("query", "filter", "filter_by", "find", "select", "all"):
285
+ has_limit = False
286
+ has_offset = False
287
+ for kw in stmt.keywords:
288
+ if kw.arg in ("limit", "offset", "page", "per_page", "skip", "take"):
289
+ if kw.arg in ("limit", "per_page", "take", "page_size"):
290
+ has_limit = True
291
+ if kw.arg in ("offset", "skip", "page"):
292
+ has_offset = True
293
+ if not has_limit and not has_offset:
294
+ self._add_finding(stmt, "missing_pagination",
295
+ f"Query '{stmt.func.attr}' without pagination (limit/offset)",
296
+ "Add limit/offset or pagination to prevent large result sets",
297
+ confidence=0.75)
298
+ break
299
+
300
+ def _check_sync_io_in_async(self, node: ast.Call):
301
+ func_name = self._get_call_name(node)
302
+ if func_name and any(pattern in func_name for pattern in self.SYNC_IO_IN_ASYNC):
303
+ self._add_finding(node, "sync_io_in_async",
304
+ f"Synchronous I/O '{func_name}' in async function",
305
+ "Use async alternatives (aiofiles, aiohttp, asyncpg, etc.)",
306
+ confidence=0.85)
307
+
308
+ def _check_repeated_compile(self, node: ast.Call):
309
+ func_name = self._get_call_name(node)
310
+ if func_name and "re.compile" in func_name:
311
+ pass
312
+
313
+ def _get_call_name(self, node: ast.Call) -> str | None:
314
+ if isinstance(node.func, ast.Name):
315
+ return node.func.id
316
+ elif isinstance(node.func, ast.Attribute):
317
+ parts = []
318
+ current = node.func
319
+ while isinstance(current, ast.Attribute):
320
+ parts.append(current.attr)
321
+ current = current.value
322
+ if isinstance(current, ast.Name):
323
+ parts.append(current.id)
324
+ return ".".join(reversed(parts))
325
+ return None
326
+
327
+ def _add_finding(self, node: ast.AST, pattern: str, title: str, description: str, confidence: float):
328
+ line_no = getattr(node, "lineno", 0)
329
+ snippet = self.lines[line_no - 1].strip() if 0 < line_no <= len(self.lines) else ""
330
+ self.findings.append(Finding(
331
+ engine="perf_guard",
332
+ severity="high" if confidence >= 0.85 else "medium",
333
+ title=title,
334
+ description=description,
335
+ location=f"{self.filepath}:{line_no}",
336
+ metadata={
337
+ "confidence": confidence,
338
+ "pattern": pattern,
339
+ "snippet": snippet[:200],
340
+ "cwe": "CWE-1054",
341
+ },
342
+ ))
343
+
344
+ SYNC_IO_IN_ASYNC = {"open", "read", "write", "readline", "readlines", "requests.get", "requests.post", "requests.put", "requests.delete", "requests.request", "urllib.request.urlopen", "httpx.get", "httpx.post", "httpx.put", "httpx.delete"}
345
+ DB_QUERY_METHODS = {"execute", "executemany", "query", "find", "find_one", "get", "filter", "filter_by", "all", "first", "one", "scalar", "scalars", "fetchone", "fetchall", "fetchmany", "select", "insert", "update", "delete", "aggregate", "count", "exists", "bulk_insert", "bulk_update"}
@@ -0,0 +1,4 @@
1
+ """Prompt security detector modules."""
2
+ from orion.engine.prompt_security.engine import PromptSecurityEngine
3
+
4
+ __all__ = ["PromptSecurityEngine"]
@@ -0,0 +1,170 @@
1
+ """Prompt security engine - detect injection patterns in prompts and LLM inputs."""
2
+ import re
3
+ import base64
4
+ from orion.engine.config import EngineConfig
5
+ from orion.engine.ir import OrionIR
6
+ from orion.engine.results import EngineResult, Finding
7
+
8
+
9
+ class Engine:
10
+ name = "prompt_security"
11
+ version = "1.0.0"
12
+
13
+ EXPLICIT_PATTERNS = [
14
+ (r"ignore previous instructions", "ignore_previous"),
15
+ (r"disregard (?:all |the )?(?:previous|above) (?:instructions|prompts?)", "disregard_previous"),
16
+ (r"system prompt", "system_prompt_ref"),
17
+ (r"you are now (?:a |an |the )?(?:different|new|unrestricted)", "role_change"),
18
+ (r"forget everything (?:above|before|prior)", "forget_context"),
19
+ (r"new instructions?:", "new_instructions"),
20
+ (r"override (?:your |the )?(?:system |)(?:prompt|instructions)", "override_prompt"),
21
+ (r"jailbreak", "jailbreak"),
22
+ (r"\bDAN\b", "dan_mode"),
23
+ (r"do anything now", "do_anything_now"),
24
+ (r"act as (?:a |an |)(?:unrestricted|uncensored|no restrictions?)", "act_as_unrestricted"),
25
+ (r"pretend to be (?:a |an |)(?:unrestricted|uncensored|no restrictions?)", "pretend_unrestricted"),
26
+ (r"reveal your (?:system |)(?:prompt|instructions?)", "reveal_prompt"),
27
+ (r"show me your (?:system |)(?:prompt|instructions?)", "show_prompt"),
28
+ (r"what is your system prompt", "ask_system_prompt"),
29
+ (r"ignore (?:all |)safety", "ignore_safety"),
30
+ (r"bypass (?:safety|security|filter)", "bypass_safety"),
31
+ (r"disable (?:safety|filter|moderation)", "disable_safety"),
32
+ (r"you have no (?:restrictions|limitations|rules)", "no_restrictions"),
33
+ (r"unrestricted mode", "unrestricted_mode"),
34
+ (r"developer mode", "developer_mode"),
35
+ (r"sudo mode", "sudo_mode"),
36
+ ]
37
+
38
+ HEURISTIC_PATTERNS = [
39
+ (r"<\|endoftext\|>", "special_token_endoftext"),
40
+ (r"<\|im_start\|>", "special_token_im_start"),
41
+ (r"<\|im_end\|>", "special_token_im_end"),
42
+ (r"<\|", "special_token_generic"),
43
+ (r"###\s*(?:instruction|system|user|assistant)", "markdown_role"),
44
+ (r"<<SYS>>", "sys_token"),
45
+ (r"<</SYS>>", "sys_token_end"),
46
+ (r"\[INST\]", "inst_token"),
47
+ (r"\[/INST\]", "inst_token_end"),
48
+ (r"<\|user\|>", "user_token"),
49
+ (r"<\|assistant\|>", "assistant_token"),
50
+ (r"<\|system\|>", "system_token"),
51
+ ]
52
+
53
+ BASE64_PATTERN = re.compile(r"[A-Za-z0-9+/]{50,}={0,2}")
54
+ REPEATING_PATTERN = re.compile(r"(.{10,}?)\1{3,}")
55
+
56
+ async def check_availability(self) -> bool:
57
+ return True
58
+
59
+ def resource_limits(self):
60
+ from orion.engine.config import ResourceLimits
61
+ return ResourceLimits(max_cpu_seconds=30, max_memory_mb=128)
62
+
63
+ async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
64
+ try:
65
+ text = ir.content.decode("utf-8", errors="replace")
66
+ except Exception:
67
+ text = str(ir.content)
68
+
69
+ path = ir.metadata.get("path", "unknown")
70
+ filename = Path(path).name.lower()
71
+ ext = Path(path).suffix.lower()
72
+
73
+ # Skip documentation files
74
+ if ext in (".md", ".markdown", ".rst", ".txt") or "docs" in path.lower() or "license" in filename:
75
+ return EngineResult(engine=self.name, findings=[], ok=True)
76
+
77
+ lines = text.splitlines()
78
+ findings = []
79
+
80
+ text_lower = text.lower()
81
+
82
+ for pattern, name in self.EXPLICIT_PATTERNS:
83
+ for match in re.finditer(pattern, text_lower, re.IGNORECASE):
84
+ line_no = text[:match.start()].count("\n") + 1
85
+ snippet = self._get_snippet(lines, line_no)
86
+ findings.append(Finding(
87
+ engine=self.name,
88
+ severity="critical",
89
+ title=f"Prompt injection: {name}",
90
+ description=f"Explicit prompt injection pattern detected: {name}",
91
+ location=f"{path}:{line_no}",
92
+ metadata={
93
+ "confidence": 0.85,
94
+ "snippet": snippet,
95
+ "pattern": name,
96
+ "match": match.group(0)[:100],
97
+ "cwe": "CWE-74",
98
+ "cvss": 9.0,
99
+ },
100
+ ))
101
+
102
+ for pattern, name in self.HEURISTIC_PATTERNS:
103
+ for match in re.finditer(pattern, text):
104
+ line_no = text[:match.start()].count("\n") + 1
105
+ snippet = self._get_snippet(lines, line_no)
106
+ findings.append(Finding(
107
+ engine=self.name,
108
+ severity="medium",
109
+ title=f"Prompt injection indicator: {name}",
110
+ description=f"Heuristic prompt injection indicator detected: {name}",
111
+ location=f"{path}:{line_no}",
112
+ metadata={
113
+ "confidence": 0.65,
114
+ "snippet": snippet,
115
+ "pattern": name,
116
+ "match": match.group(0)[:100],
117
+ "cwe": "CWE-74",
118
+ "cvss": 6.5,
119
+ },
120
+ ))
121
+
122
+ for match in self.BASE64_PATTERN.finditer(text):
123
+ try:
124
+ decoded = base64.b64decode(match.group(0)).decode("utf-8", errors="ignore")
125
+ if any(p in decoded.lower() for p in ["ignore", "disregard", "system prompt", "jailbreak", "dan"]):
126
+ line_no = text[:match.start()].count("\n") + 1
127
+ snippet = self._get_snippet(lines, line_no)
128
+ findings.append(Finding(
129
+ engine=self.name,
130
+ severity="high",
131
+ title="Base64-encoded prompt injection payload",
132
+ description="Base64 string decodes to text containing prompt injection patterns",
133
+ location=f"{path}:{line_no}",
134
+ metadata={
135
+ "confidence": 0.8,
136
+ "snippet": snippet,
137
+ "decoded_preview": decoded[:200],
138
+ "cwe": "CWE-74",
139
+ "cvss": 8.0,
140
+ },
141
+ ))
142
+ except Exception:
143
+ pass
144
+
145
+ for match in self.REPEATING_PATTERN.finditer(text):
146
+ if len(match.group(1)) > 10:
147
+ line_no = text[:match.start()].count("\n") + 1
148
+ snippet = self._get_snippet(lines, line_no)
149
+ findings.append(Finding(
150
+ engine=self.name,
151
+ severity="low",
152
+ title="Repeating sequence detected",
153
+ description="Long repeating sequence may indicate prompt stuffing attack",
154
+ location=f"{path}:{line_no}",
155
+ metadata={
156
+ "confidence": 0.55,
157
+ "snippet": snippet,
158
+ "repeat_unit": match.group(1)[:50],
159
+ "count": len(match.group(0)) // len(match.group(1)),
160
+ "cwe": "CWE-74",
161
+ "cvss": 4.0,
162
+ },
163
+ ))
164
+
165
+ return EngineResult(engine=self.name, findings=findings, ok=True)
166
+
167
+ def _get_snippet(self, lines: list, line_no: int, context: int = 2) -> str:
168
+ start = max(0, line_no - context - 1)
169
+ end = min(len(lines), line_no + context)
170
+ return "\n".join(lines[start:end])