@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,558 +0,0 @@
|
|
|
1
|
-
"""Python linting engine - AST-based comprehensive linting without external tools."""
|
|
2
|
-
import ast
|
|
3
|
-
import re
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import List, Dict, Set, Optional, Any
|
|
6
|
-
from collections import defaultdict
|
|
7
|
-
|
|
8
|
-
from orion.engine.config import EngineConfig
|
|
9
|
-
from orion.engine.ir import OrionIR
|
|
10
|
-
from orion.engine.results import EngineResult, Finding
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import builtins
|
|
14
|
-
import sys
|
|
15
|
-
|
|
16
|
-
class LintEngine:
|
|
17
|
-
name = "lint"
|
|
18
|
-
version = "1.0.0"
|
|
19
|
-
|
|
20
|
-
STDLIB_MODULES = {
|
|
21
|
-
"os", "sys", "json", "re", "ast", "typing", "collections", "itertools",
|
|
22
|
-
"functools", "dataclasses", "enum", "pathlib", "datetime", "time",
|
|
23
|
-
"math", "random", "string", "hashlib", "hmac", "secrets", "uuid",
|
|
24
|
-
"subprocess", "threading", "multiprocessing", "asyncio", "socket",
|
|
25
|
-
"urllib", "http", "email", "html", "xml", "csv", "sqlite3", "pickle",
|
|
26
|
-
"copy", "pprint", "textwrap", "unicodedata", "fractions", "decimal",
|
|
27
|
-
"statistics", "inspect", "importlib", "pkgutil", "runpy", "traceback",
|
|
28
|
-
"warnings", "contextlib", "abc", "numbers", "operator", "keyword",
|
|
29
|
-
"tokenize", "token", "parser", "symbol", "ast", "sysconfig", "site",
|
|
30
|
-
"builtins", "__future__", "__main__", "argparse", "getopt", "optparse",
|
|
31
|
-
"shlex", "cmd", "readline", "rlcompleter", "logging", "logging.config",
|
|
32
|
-
"logging.handlers", "configparser", "netrc", "xdrlib", "plistlib",
|
|
33
|
-
"base64", "binascii", "quopri", "uu", "zlib", "gzip", "bz2", "lzma",
|
|
34
|
-
"zipfile", "tarfile", "tomllib", "tomli", "signal", "mmap", "errno",
|
|
35
|
-
"ctypes", "concurrent", "queue", "contextvars"
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
THIRD_PARTY_INDICATORS = {
|
|
39
|
-
"django", "flask", "fastapi", "requests", "httpx", "aiohttp",
|
|
40
|
-
"sqlalchemy", "pydantic", "numpy", "pandas", "scipy", "matplotlib",
|
|
41
|
-
"pytest", "unittest", "celery", "redis", "psycopg2", "pymongo",
|
|
42
|
-
"boto3", "google", "azure", "aws", "kubernetes", "docker",
|
|
43
|
-
"rich", "click", "typer", "jinja2", "pyyaml", "cryptography"
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async def check_availability(self) -> bool:
|
|
47
|
-
return True
|
|
48
|
-
|
|
49
|
-
def resource_limits(self):
|
|
50
|
-
from orion.engine.config import ResourceLimits
|
|
51
|
-
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
|
|
52
|
-
|
|
53
|
-
def _should_exclude(self, filepath: str, config: EngineConfig) -> bool:
|
|
54
|
-
path = Path(filepath)
|
|
55
|
-
if config.toggles.get("exclude"):
|
|
56
|
-
for pattern in config.toggles["exclude"]:
|
|
57
|
-
if path.match(pattern) or any(p.match(pattern) for p in path.parents):
|
|
58
|
-
return True
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
class LintVisitor(ast.NodeVisitor):
|
|
62
|
-
def __init__(self, content: str, filepath: str, project_root: str):
|
|
63
|
-
self.content = content
|
|
64
|
-
self.filepath = filepath
|
|
65
|
-
self.project_root = project_root
|
|
66
|
-
self.lines = content.splitlines()
|
|
67
|
-
self.findings = []
|
|
68
|
-
self.scope_stack = [set()] # Stack of variable scopes
|
|
69
|
-
self.imports = [] # List of (line, module, names, is_from)
|
|
70
|
-
self.assignments = defaultdict(list) # var_name -> list of (line, node)
|
|
71
|
-
self.usages = defaultdict(list) # var_name -> list of (line, node)
|
|
72
|
-
self.function_defs = [] # List of (name, line, node, is_public)
|
|
73
|
-
self.class_defs = [] # List of (name, line, node, methods, bases)
|
|
74
|
-
self.current_function = None
|
|
75
|
-
self.current_class = None
|
|
76
|
-
self.builtins = set(dir(builtins)) | {
|
|
77
|
-
"__file__", "__name__", "__doc__", "__package__", "__loader__",
|
|
78
|
-
"__spec__", "__annotations__", "__builtins__", "__all__", "__path__",
|
|
79
|
-
"__class__", "__dict__", "cls", "self"
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
def _get_snippet(self, line_num: int, context: int = 2) -> str:
|
|
83
|
-
start = max(0, line_num - context - 1)
|
|
84
|
-
end = min(len(self.lines), line_num + context)
|
|
85
|
-
return '\n'.join(self.lines[start:end])
|
|
86
|
-
|
|
87
|
-
def _add_finding(self, severity: str, title: str, description: str, line: int, metadata: dict):
|
|
88
|
-
self.findings.append({
|
|
89
|
-
"severity": severity,
|
|
90
|
-
"title": title,
|
|
91
|
-
"description": description,
|
|
92
|
-
"line": line,
|
|
93
|
-
"snippet": self._get_snippet(line),
|
|
94
|
-
"metadata": metadata,
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
def push_scope(self):
|
|
98
|
-
self.scope_stack.append(set())
|
|
99
|
-
|
|
100
|
-
def pop_scope(self):
|
|
101
|
-
if len(self.scope_stack) > 1:
|
|
102
|
-
self.scope_stack.pop()
|
|
103
|
-
|
|
104
|
-
def add_var(self, name: str, line: int, node: ast.AST):
|
|
105
|
-
if name:
|
|
106
|
-
self.scope_stack[-1].add(name)
|
|
107
|
-
self.assignments[name].append((line, node))
|
|
108
|
-
|
|
109
|
-
def use_var(self, name: str, line: int, node: ast.AST):
|
|
110
|
-
if name:
|
|
111
|
-
self.usages[name].append((line, node))
|
|
112
|
-
|
|
113
|
-
def is_defined(self, name: str) -> bool:
|
|
114
|
-
if not name:
|
|
115
|
-
return True
|
|
116
|
-
for scope in reversed(self.scope_stack):
|
|
117
|
-
if name in scope:
|
|
118
|
-
return True
|
|
119
|
-
return name in self.builtins
|
|
120
|
-
|
|
121
|
-
def _extract_target_names(self, node: ast.AST) -> List[str]:
|
|
122
|
-
names = []
|
|
123
|
-
if isinstance(node, ast.Name):
|
|
124
|
-
names.append(node.id)
|
|
125
|
-
elif isinstance(node, (ast.Tuple, ast.List)):
|
|
126
|
-
for elt in node.elts:
|
|
127
|
-
names.extend(self._extract_target_names(elt))
|
|
128
|
-
elif isinstance(node, ast.Starred):
|
|
129
|
-
names.extend(self._extract_target_names(node.value))
|
|
130
|
-
return names
|
|
131
|
-
|
|
132
|
-
def visit_Module(self, node: ast.Module):
|
|
133
|
-
self.generic_visit(node)
|
|
134
|
-
|
|
135
|
-
def visit_FunctionDef(self, node: ast.FunctionDef):
|
|
136
|
-
is_public = not node.name.startswith('_')
|
|
137
|
-
self.function_defs.append((node.name, node.lineno, node, is_public))
|
|
138
|
-
self.add_var(node.name, node.lineno, node)
|
|
139
|
-
|
|
140
|
-
for default in node.args.defaults:
|
|
141
|
-
if isinstance(default, (ast.List, ast.Dict, ast.Set)):
|
|
142
|
-
self._add_finding(
|
|
143
|
-
"medium",
|
|
144
|
-
f"Mutable default argument in function '{node.name}'",
|
|
145
|
-
"Mutable default arguments retain state across invocations and can cause unexpected bugs",
|
|
146
|
-
node.lineno,
|
|
147
|
-
{"rule": "mutable_default_argument", "function": node.name, "type": type(default).__name__},
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
self.push_scope()
|
|
151
|
-
self.current_function = node.name
|
|
152
|
-
for arg in node.args.args:
|
|
153
|
-
self.add_var(arg.arg, arg.lineno, arg)
|
|
154
|
-
for arg in getattr(node.args, 'posonlyargs', []):
|
|
155
|
-
self.add_var(arg.arg, arg.lineno, arg)
|
|
156
|
-
for arg in getattr(node.args, 'kwonlyargs', []):
|
|
157
|
-
self.add_var(arg.arg, arg.lineno, arg)
|
|
158
|
-
if node.args.vararg:
|
|
159
|
-
self.add_var(node.args.vararg.arg, node.args.vararg.lineno, node.args.vararg)
|
|
160
|
-
if node.args.kwarg:
|
|
161
|
-
self.add_var(node.args.kwarg.arg, node.args.kwarg.lineno, node.args.kwarg)
|
|
162
|
-
|
|
163
|
-
self.generic_visit(node)
|
|
164
|
-
self.pop_scope()
|
|
165
|
-
self.current_function = None
|
|
166
|
-
|
|
167
|
-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
|
|
168
|
-
self.visit_FunctionDef(node)
|
|
169
|
-
|
|
170
|
-
def visit_ClassDef(self, node: ast.ClassDef):
|
|
171
|
-
is_public = not node.name.startswith('_')
|
|
172
|
-
self.add_var(node.name, node.lineno, node)
|
|
173
|
-
methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
174
|
-
self.class_defs.append((node.name, node.lineno, node, methods, node.bases))
|
|
175
|
-
|
|
176
|
-
self.push_scope()
|
|
177
|
-
self.current_class = node.name
|
|
178
|
-
self.generic_visit(node)
|
|
179
|
-
self.pop_scope()
|
|
180
|
-
self.current_class = None
|
|
181
|
-
|
|
182
|
-
def visit_Assign(self, node: ast.Assign):
|
|
183
|
-
for target in node.targets:
|
|
184
|
-
for name in self._extract_target_names(target):
|
|
185
|
-
self.add_var(name, node.lineno, node)
|
|
186
|
-
self.generic_visit(node)
|
|
187
|
-
|
|
188
|
-
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
189
|
-
if node.target:
|
|
190
|
-
for name in self._extract_target_names(node.target):
|
|
191
|
-
self.add_var(name, node.lineno, node)
|
|
192
|
-
self.generic_visit(node)
|
|
193
|
-
|
|
194
|
-
def visit_AugAssign(self, node: ast.AugAssign):
|
|
195
|
-
if isinstance(node.target, ast.Name):
|
|
196
|
-
self.use_var(node.target.id, node.lineno, node)
|
|
197
|
-
self.generic_visit(node)
|
|
198
|
-
|
|
199
|
-
def visit_For(self, node: ast.For):
|
|
200
|
-
for name in self._extract_target_names(node.target):
|
|
201
|
-
self.add_var(name, node.lineno, node)
|
|
202
|
-
self.generic_visit(node)
|
|
203
|
-
|
|
204
|
-
def visit_AsyncFor(self, node: ast.AsyncFor):
|
|
205
|
-
for name in self._extract_target_names(node.target):
|
|
206
|
-
self.add_var(name, node.lineno, node)
|
|
207
|
-
self.generic_visit(node)
|
|
208
|
-
|
|
209
|
-
def visit_With(self, node: ast.With):
|
|
210
|
-
for item in node.items:
|
|
211
|
-
if item.optional_vars:
|
|
212
|
-
for name in self._extract_target_names(item.optional_vars):
|
|
213
|
-
self.add_var(name, node.lineno, node)
|
|
214
|
-
self.generic_visit(node)
|
|
215
|
-
|
|
216
|
-
def visit_AsyncWith(self, node: ast.AsyncWith):
|
|
217
|
-
for item in node.items:
|
|
218
|
-
if item.optional_vars:
|
|
219
|
-
for name in self._extract_target_names(item.optional_vars):
|
|
220
|
-
self.add_var(name, node.lineno, node)
|
|
221
|
-
self.generic_visit(node)
|
|
222
|
-
|
|
223
|
-
def visit_ExceptHandler(self, node: ast.ExceptHandler):
|
|
224
|
-
if node.name:
|
|
225
|
-
self.add_var(node.name, node.lineno, node)
|
|
226
|
-
self.generic_visit(node)
|
|
227
|
-
|
|
228
|
-
def visit_comprehension(self, node: ast.comprehension):
|
|
229
|
-
for name in self._extract_target_names(node.target):
|
|
230
|
-
self.add_var(name, getattr(node.target, 'lineno', 1), node)
|
|
231
|
-
self.generic_visit(node)
|
|
232
|
-
|
|
233
|
-
def visit_Name(self, node: ast.Name):
|
|
234
|
-
if isinstance(node.ctx, ast.Load):
|
|
235
|
-
self.use_var(node.id, node.lineno, node)
|
|
236
|
-
if not self.is_defined(node.id):
|
|
237
|
-
self._add_finding(
|
|
238
|
-
"medium",
|
|
239
|
-
f"Undefined name '{node.id}'",
|
|
240
|
-
f"Name '{node.id}' is used but not defined in visible scopes",
|
|
241
|
-
node.lineno,
|
|
242
|
-
{"rule": "undefined_name", "name": node.id, "cwe": "CWE-476"},
|
|
243
|
-
)
|
|
244
|
-
elif isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
245
|
-
self.add_var(node.id, node.lineno, node)
|
|
246
|
-
self.generic_visit(node)
|
|
247
|
-
|
|
248
|
-
def visit_Import(self, node: ast.Import):
|
|
249
|
-
for alias in node.names:
|
|
250
|
-
name = alias.asname or alias.name.split('.')[0]
|
|
251
|
-
self.add_var(name, node.lineno, node)
|
|
252
|
-
self.imports.append((node.lineno, alias.name, [alias.asname or alias.name], False))
|
|
253
|
-
self.generic_visit(node)
|
|
254
|
-
|
|
255
|
-
def visit_ImportFrom(self, node: ast.ImportFrom):
|
|
256
|
-
module = node.module or ""
|
|
257
|
-
names = []
|
|
258
|
-
for alias in node.names:
|
|
259
|
-
if alias.name != "*":
|
|
260
|
-
name = alias.asname or alias.name
|
|
261
|
-
self.add_var(name, node.lineno, node)
|
|
262
|
-
names.append(name)
|
|
263
|
-
else:
|
|
264
|
-
names.append("*")
|
|
265
|
-
self.imports.append((node.lineno, module, names, True))
|
|
266
|
-
if any(n == '*' for n in names):
|
|
267
|
-
self._add_finding(
|
|
268
|
-
"medium",
|
|
269
|
-
f"Wildcard import from '{module}'",
|
|
270
|
-
"Wildcard imports make it unclear what names are available",
|
|
271
|
-
node.lineno,
|
|
272
|
-
{"rule": "wildcard_import", "module": module},
|
|
273
|
-
)
|
|
274
|
-
self.generic_visit(node)
|
|
275
|
-
|
|
276
|
-
def visit_Compare(self, node: ast.Compare):
|
|
277
|
-
# Check for comparison to None using ==/!=
|
|
278
|
-
if len(node.ops) == 1 and len(node.comparators) == 1:
|
|
279
|
-
op = node.ops[0]
|
|
280
|
-
comp = node.comparators[0]
|
|
281
|
-
if isinstance(comp, ast.Constant) and comp.value is None:
|
|
282
|
-
if isinstance(op, (ast.Eq, ast.NotEq)):
|
|
283
|
-
self._add_finding(
|
|
284
|
-
"medium",
|
|
285
|
-
"Comparison to None using ==/!=",
|
|
286
|
-
"Use 'is None' or 'is not None' instead",
|
|
287
|
-
node.lineno,
|
|
288
|
-
{"rule": "comparison_to_none", "operator": "==" if isinstance(op, ast.Eq) else "!="},
|
|
289
|
-
)
|
|
290
|
-
self.generic_visit(node)
|
|
291
|
-
|
|
292
|
-
def visit_Try(self, node: ast.Try):
|
|
293
|
-
for handler in node.handlers:
|
|
294
|
-
if handler.type is None:
|
|
295
|
-
# Bare except:
|
|
296
|
-
self._add_finding(
|
|
297
|
-
"medium",
|
|
298
|
-
"Bare except: clause",
|
|
299
|
-
"Bare except catches all exceptions including SystemExit and KeyboardInterrupt",
|
|
300
|
-
handler.lineno,
|
|
301
|
-
{"rule": "bare_except"},
|
|
302
|
-
)
|
|
303
|
-
self.generic_visit(node)
|
|
304
|
-
|
|
305
|
-
def visit_Call(self, node: ast.Call):
|
|
306
|
-
# Check for eval/exec usage
|
|
307
|
-
if isinstance(node.func, ast.Name):
|
|
308
|
-
if node.func.id in ("eval", "exec"):
|
|
309
|
-
self._add_finding(
|
|
310
|
-
"high",
|
|
311
|
-
f"Use of {node.func.id}()",
|
|
312
|
-
f"{node.func.id}() can execute arbitrary code - security risk",
|
|
313
|
-
node.lineno,
|
|
314
|
-
{"rule": f"{node.func.id}_usage"},
|
|
315
|
-
)
|
|
316
|
-
self.generic_visit(node)
|
|
317
|
-
|
|
318
|
-
def _check_unused_variables(self, visitor: 'LintEngine.LintVisitor') -> List[dict]:
|
|
319
|
-
findings = []
|
|
320
|
-
for var_name, assignments in visitor.assignments.items():
|
|
321
|
-
usages = visitor.usages.get(var_name, [])
|
|
322
|
-
if not usages:
|
|
323
|
-
# Check if it's a legitimate unused (e.g., _ prefix, or in __all__)
|
|
324
|
-
if var_name.startswith('_') and len(var_name) == 1:
|
|
325
|
-
continue # Single underscore is often used as throwaway
|
|
326
|
-
if var_name.startswith('__') and var_name.endswith('__'):
|
|
327
|
-
continue # Dunder methods/vars
|
|
328
|
-
for line, node in assignments:
|
|
329
|
-
findings.append({
|
|
330
|
-
"severity": "low",
|
|
331
|
-
"title": f"Unused variable '{var_name}'",
|
|
332
|
-
"description": f"Variable '{var_name}' is assigned but never used",
|
|
333
|
-
"line": line,
|
|
334
|
-
"snippet": visitor._get_snippet(line),
|
|
335
|
-
"metadata": {"rule": "unused_variable", "variable": var_name},
|
|
336
|
-
})
|
|
337
|
-
return findings
|
|
338
|
-
|
|
339
|
-
def _check_import_order(self, visitor: 'LintEngine.LintVisitor') -> List[dict]:
|
|
340
|
-
findings = []
|
|
341
|
-
if not visitor.imports:
|
|
342
|
-
return findings
|
|
343
|
-
|
|
344
|
-
# Categorize imports
|
|
345
|
-
stdlib_imports = []
|
|
346
|
-
third_party_imports = []
|
|
347
|
-
local_imports = []
|
|
348
|
-
|
|
349
|
-
for line, module, names, is_from in visitor.imports:
|
|
350
|
-
if module.startswith('.'):
|
|
351
|
-
local_imports.append((line, module, names, is_from))
|
|
352
|
-
elif module.split('.')[0] in self.STDLIB_MODULES:
|
|
353
|
-
stdlib_imports.append((line, module, names, is_from))
|
|
354
|
-
else:
|
|
355
|
-
third_party_imports.append((line, module, names, is_from))
|
|
356
|
-
|
|
357
|
-
# Check order: stdlib -> third-party -> local
|
|
358
|
-
all_imports = stdlib_imports + third_party_imports + local_imports
|
|
359
|
-
expected_lines = [imp[0] for imp in all_imports]
|
|
360
|
-
actual_lines = [imp[0] for imp in visitor.imports]
|
|
361
|
-
|
|
362
|
-
# Simple check: stdlib imports should come before third-party, etc.
|
|
363
|
-
last_stdlib = max([imp[0] for imp in stdlib_imports], default=0)
|
|
364
|
-
first_third = min([imp[0] for imp in third_party_imports], default=float('inf'))
|
|
365
|
-
last_third = max([imp[0] for imp in third_party_imports], default=0)
|
|
366
|
-
first_local = min([imp[0] for imp in local_imports], default=float('inf'))
|
|
367
|
-
|
|
368
|
-
if last_stdlib > first_third and first_third != float('inf'):
|
|
369
|
-
findings.append({
|
|
370
|
-
"severity": "low",
|
|
371
|
-
"title": "Import order violation: stdlib after third-party",
|
|
372
|
-
"description": "Standard library imports should come before third-party imports",
|
|
373
|
-
"line": first_third,
|
|
374
|
-
"snippet": visitor._get_snippet(first_third),
|
|
375
|
-
"metadata": {"rule": "import_order", "issue": "stdlib_after_third_party"},
|
|
376
|
-
})
|
|
377
|
-
|
|
378
|
-
if last_third > first_local and first_local != float('inf'):
|
|
379
|
-
findings.append({
|
|
380
|
-
"severity": "low",
|
|
381
|
-
"title": "Import order violation: third-party after local",
|
|
382
|
-
"description": "Third-party imports should come before local imports",
|
|
383
|
-
"line": first_local,
|
|
384
|
-
"snippet": visitor._get_snippet(first_local),
|
|
385
|
-
"metadata": {"rule": "import_order", "issue": "third_party_after_local"},
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
# Check for blank lines between groups
|
|
389
|
-
if stdlib_imports and third_party_imports:
|
|
390
|
-
last_stdlib_line = max(imp[0] for imp in stdlib_imports)
|
|
391
|
-
first_third_line = min(imp[0] for imp in third_party_imports)
|
|
392
|
-
if first_third_line - last_stdlib_line == 1:
|
|
393
|
-
# Check if there's a blank line
|
|
394
|
-
if visitor.lines[last_stdlib_line].strip() != "":
|
|
395
|
-
findings.append({
|
|
396
|
-
"severity": "low",
|
|
397
|
-
"title": "Missing blank line between import groups",
|
|
398
|
-
"description": "Add a blank line between standard library and third-party imports",
|
|
399
|
-
"line": first_third_line,
|
|
400
|
-
"snippet": visitor._get_snippet(first_third_line),
|
|
401
|
-
"metadata": {"rule": "import_order", "issue": "missing_blank_line"},
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
return findings
|
|
405
|
-
|
|
406
|
-
def _check_line_length(self, visitor: 'LintEngine.LintVisitor') -> List[dict]:
|
|
407
|
-
findings = []
|
|
408
|
-
for i, line in enumerate(visitor.lines, 1):
|
|
409
|
-
if len(line) > 100:
|
|
410
|
-
findings.append({
|
|
411
|
-
"severity": "low",
|
|
412
|
-
"title": f"Line too long ({len(line)} > 100)",
|
|
413
|
-
"description": f"Line exceeds maximum recommended length of 100 characters",
|
|
414
|
-
"line": i,
|
|
415
|
-
"snippet": visitor._get_snippet(i, 1),
|
|
416
|
-
"metadata": {"rule": "line_length", "length": len(line)},
|
|
417
|
-
})
|
|
418
|
-
return findings
|
|
419
|
-
|
|
420
|
-
def _check_trailing_whitespace(self, visitor: 'LintEngine.LintVisitor') -> List[dict]:
|
|
421
|
-
findings = []
|
|
422
|
-
for i, line in enumerate(visitor.lines, 1):
|
|
423
|
-
if line.rstrip() != line:
|
|
424
|
-
findings.append({
|
|
425
|
-
"severity": "low",
|
|
426
|
-
"title": "Trailing whitespace",
|
|
427
|
-
"description": "Line has trailing whitespace",
|
|
428
|
-
"line": i,
|
|
429
|
-
"snippet": visitor._get_snippet(i, 1),
|
|
430
|
-
"metadata": {"rule": "trailing_whitespace"},
|
|
431
|
-
})
|
|
432
|
-
return findings
|
|
433
|
-
|
|
434
|
-
def _check_init_not_empty(self, visitor: 'LintEngine.LintVisitor') -> List[dict]:
|
|
435
|
-
findings = []
|
|
436
|
-
if Path(visitor.filepath).name == "__init__.py":
|
|
437
|
-
# Check if it's essentially empty (only comments/docstrings)
|
|
438
|
-
non_empty_lines = [l for l in visitor.lines if l.strip() and not l.strip().startswith('#')]
|
|
439
|
-
if len(non_empty_lines) <= 1: # Allow just a docstring
|
|
440
|
-
# Check if it's a namespace package (has __path__ manipulation)
|
|
441
|
-
has_namespace = any("__path__" in l for l in visitor.lines)
|
|
442
|
-
if not has_namespace:
|
|
443
|
-
findings.append({
|
|
444
|
-
"severity": "low",
|
|
445
|
-
"title": "Empty __init__.py",
|
|
446
|
-
"description": "__init__.py should not be empty unless it's a namespace package",
|
|
447
|
-
"line": 1,
|
|
448
|
-
"snippet": visitor._get_snippet(1),
|
|
449
|
-
"metadata": {"rule": "empty_init"},
|
|
450
|
-
})
|
|
451
|
-
return findings
|
|
452
|
-
|
|
453
|
-
def _check_god_class(self, visitor: 'LintEngine.LintVisitor') -> List[dict]:
|
|
454
|
-
findings = []
|
|
455
|
-
for class_name, line, node, methods, bases in visitor.class_defs:
|
|
456
|
-
if len(methods) > 20:
|
|
457
|
-
findings.append({
|
|
458
|
-
"severity": "medium",
|
|
459
|
-
"title": f"God class: '{class_name}' has {len(methods)} methods",
|
|
460
|
-
"description": f"Class has too many methods ({len(methods)} > 20), consider splitting",
|
|
461
|
-
"line": line,
|
|
462
|
-
"snippet": visitor._get_snippet(line),
|
|
463
|
-
"metadata": {"rule": "god_class", "class": class_name, "method_count": len(methods)},
|
|
464
|
-
})
|
|
465
|
-
# Check lines of code in class
|
|
466
|
-
if hasattr(node, 'end_lineno') and node.end_lineno:
|
|
467
|
-
class_lines = node.end_lineno - node.lineno + 1
|
|
468
|
-
if class_lines > 500:
|
|
469
|
-
findings.append({
|
|
470
|
-
"severity": "medium",
|
|
471
|
-
"title": f"God class: '{class_name}' has {class_lines} lines",
|
|
472
|
-
"description": f"Class is too long ({class_lines} > 500 lines), consider splitting",
|
|
473
|
-
"line": line,
|
|
474
|
-
"snippet": visitor._get_snippet(line),
|
|
475
|
-
"metadata": {"rule": "god_class_lines", "class": class_name, "line_count": class_lines},
|
|
476
|
-
})
|
|
477
|
-
return findings
|
|
478
|
-
|
|
479
|
-
def _check_long_function(self, visitor: 'LintEngine.LintVisitor') -> List[dict]:
|
|
480
|
-
findings = []
|
|
481
|
-
for func_name, line, node, is_public in visitor.function_defs:
|
|
482
|
-
if hasattr(node, 'end_lineno') and node.end_lineno:
|
|
483
|
-
func_lines = node.end_lineno - node.lineno + 1
|
|
484
|
-
if func_lines > 50:
|
|
485
|
-
findings.append({
|
|
486
|
-
"severity": "low",
|
|
487
|
-
"title": f"Long function: '{func_name}' has {func_lines} lines",
|
|
488
|
-
"description": f"Function is too long ({func_lines} > 50 lines), consider refactoring",
|
|
489
|
-
"line": line,
|
|
490
|
-
"snippet": visitor._get_snippet(line),
|
|
491
|
-
"metadata": {"rule": "long_function", "function": func_name, "line_count": func_lines},
|
|
492
|
-
})
|
|
493
|
-
return findings
|
|
494
|
-
|
|
495
|
-
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
496
|
-
filepath = ir.metadata.get("path", "unknown")
|
|
497
|
-
|
|
498
|
-
if not filepath.endswith(".py"):
|
|
499
|
-
return EngineResult(engine=self.name, ok=True, metrics={"skipped": "not_python"})
|
|
500
|
-
|
|
501
|
-
if self._should_exclude(filepath, config):
|
|
502
|
-
return EngineResult(engine=self.name, ok=True, metrics={"skipped": "excluded"})
|
|
503
|
-
|
|
504
|
-
try:
|
|
505
|
-
content = ir.content.decode("utf-8", errors="replace")
|
|
506
|
-
except Exception:
|
|
507
|
-
content = str(ir.content)
|
|
508
|
-
|
|
509
|
-
project_root = ir.metadata.get("project_root", ".")
|
|
510
|
-
|
|
511
|
-
visitor = self.LintVisitor(content, filepath, project_root)
|
|
512
|
-
try:
|
|
513
|
-
tree = ast.parse(content)
|
|
514
|
-
visitor.visit(tree)
|
|
515
|
-
except SyntaxError as e:
|
|
516
|
-
return EngineResult(
|
|
517
|
-
engine=self.name,
|
|
518
|
-
findings=[Finding(
|
|
519
|
-
engine=self.name,
|
|
520
|
-
severity="error",
|
|
521
|
-
title="Syntax error in file",
|
|
522
|
-
description=f"Cannot parse file due to syntax error: {e}",
|
|
523
|
-
location=f"{filepath}:{e.lineno}",
|
|
524
|
-
metadata={"rule": "syntax_error"},
|
|
525
|
-
)],
|
|
526
|
-
ok=True,
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
# Run essential code quality checks only
|
|
530
|
-
all_findings = visitor.findings
|
|
531
|
-
all_findings.extend(self._check_unused_variables(visitor))
|
|
532
|
-
all_findings.extend(self._check_god_class(visitor))
|
|
533
|
-
|
|
534
|
-
# Convert to Finding objects
|
|
535
|
-
findings = []
|
|
536
|
-
for f in all_findings:
|
|
537
|
-
findings.append(Finding(
|
|
538
|
-
engine=self.name,
|
|
539
|
-
severity=f["severity"],
|
|
540
|
-
title=f["title"],
|
|
541
|
-
description=f["description"],
|
|
542
|
-
location=f"{filepath}:{f['line']}",
|
|
543
|
-
metadata={**f["metadata"], "file": filepath, "line": f["line"], "snippet": f["snippet"]},
|
|
544
|
-
))
|
|
545
|
-
|
|
546
|
-
return EngineResult(
|
|
547
|
-
engine=self.name,
|
|
548
|
-
findings=findings,
|
|
549
|
-
ok=True,
|
|
550
|
-
metrics={
|
|
551
|
-
"file": filepath,
|
|
552
|
-
"checks_run": 10,
|
|
553
|
-
"findings_count": len(findings),
|
|
554
|
-
},
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
Engine = LintEngine
|
|
Binary file
|