@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.
- package/LICENSE +21 -0
- package/README.md +248 -0
- package/assets/logo.png +0 -0
- package/bin/orion.js +48 -0
- package/docs/API_REFERENCE.md +211 -0
- package/docs/ARCHITECTURE.md +183 -0
- package/docs/CHANGELOG.md +112 -0
- package/docs/CONTRIBUTING.md +157 -0
- package/docs/ENGINE_CATALOG.md +87 -0
- package/docs/GETTING_STARTED.md +226 -0
- package/package.json +48 -0
- package/pyproject.toml +16 -0
- package/src/orion/__init__.py +0 -0
- package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/orion/engine/__init__.py +17 -0
- 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 +125 -0
- package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/architecture/engine.py +463 -0
- package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/authorization/engine.py +337 -0
- package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/callgraph/engine.py +236 -0
- package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/cfg/engine.py +509 -0
- package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos/engine.py +268 -0
- package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/chaos_config/engine.py +322 -0
- package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/complexity/engine.py +144 -0
- package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/compliance/engine.py +325 -0
- package/src/orion/engine/config.py +41 -0
- package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_engine/engine.py +364 -0
- package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/config_validation/engine.py +313 -0
- package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/container/engine.py +546 -0
- package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/contract_probe/engine.py +316 -0
- package/src/orion/engine/core.py +75 -0
- package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/crypto_guard/engine.py +134 -0
- package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/data_residency/engine.py +238 -0
- package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dataflow/engine.py +398 -0
- package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/dead_import/engine.py +293 -0
- package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/deps/engine.py +244 -0
- package/src/orion/engine/dispatcher.py +350 -0
- package/src/orion/engine/docs/AUDIT_PROMPT.md +1248 -0
- package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/duplication/engine.py +120 -0
- package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/error_handling/engine.py +256 -0
- package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/fp_learn/engine.py +248 -0
- package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/hunter/engine.py +324 -0
- package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/iac_security/engine.py +218 -0
- package/src/orion/engine/ir.py +22 -0
- package/src/orion/engine/languages.py +454 -0
- package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/license/engine.py +172 -0
- package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/lint/engine.py +558 -0
- package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/llm_security/engine.py +125 -0
- package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/logging_c/engine.py +164 -0
- package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/noise/engine.py +140 -0
- package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/orphan/engine.py +244 -0
- package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/perf_guard/engine.py +345 -0
- package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/prompt_security/detectors/__init__.py +4 -0
- package/src/orion/engine/prompt_security/engine.py +170 -0
- package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/provenance/engine.py +242 -0
- package/src/orion/engine/registry.py +55 -0
- package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/regression/engine.py +213 -0
- package/src/orion/engine/results.py +21 -0
- package/src/orion/engine/runner.py +1310 -0
- package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sandbox_escape/engine.py +284 -0
- package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/sast/engine.py +217 -0
- package/src/orion/engine/sast/patterns/__init__.py +513 -0
- 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 +77 -0
- package/src/orion/engine/secrets/patterns/__init__.py +74 -0
- 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 +143 -0
- package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/state/engine.py +218 -0
- package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/supply_chain_sbom/engine.py +290 -0
- package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/symbol/engine.py +463 -0
- package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/taint/engine.py +464 -0
- package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/test_coverage/engine.py +445 -0
- package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/threat_model/engine.py +126 -0
- package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
- package/src/orion/engine/wiring_trace/engine.py +200 -0
- package/src/orion_engine.egg-info/PKG-INFO +7 -0
- package/src/orion_engine.egg-info/SOURCES.txt +65 -0
- package/src/orion_engine.egg-info/dependency_links.txt +1 -0
- package/src/orion_engine.egg-info/entry_points.txt +2 -0
- package/src/orion_engine.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Universal polyglot dispatcher - routes any file to the right language analyzer."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from typing import Dict, List, Optional, Any
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from orion.engine.languages import (
|
|
10
|
+
Language, detect_language, get_capabilities, LANGUAGE_CAPABILITIES
|
|
11
|
+
)
|
|
12
|
+
from orion.engine.ir import OrionIR
|
|
13
|
+
from orion.engine.config import EngineConfig
|
|
14
|
+
from orion.engine.results import EngineResult, Finding
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PolyglotDispatcher:
|
|
18
|
+
"""Routes files to language-specific analyzers for any engine."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._analyzers: Dict[str, Dict[Language, Any]] = {} # engine_name -> {Language: analyzer}
|
|
22
|
+
self._fallback_analyzers: Dict[str, Any] = {} # engine_name -> generic analyzer
|
|
23
|
+
|
|
24
|
+
def register_analyzer(self, engine_name: str, language: Language, analyzer: Any):
|
|
25
|
+
"""Register a language-specific analyzer for an engine."""
|
|
26
|
+
if engine_name not in self._analyzers:
|
|
27
|
+
self._analyzers[engine_name] = {}
|
|
28
|
+
self._analyzers[engine_name][language] = analyzer
|
|
29
|
+
|
|
30
|
+
def register_fallback(self, engine_name: str, analyzer: Any):
|
|
31
|
+
"""Register a generic fallback analyzer for unsupported languages."""
|
|
32
|
+
self._fallback_analyzers[engine_name] = analyzer
|
|
33
|
+
|
|
34
|
+
async def analyze(self, engine_name: str, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
35
|
+
"""Analyze a single IR with the appropriate language analyzer."""
|
|
36
|
+
language = ir.metadata.get("language")
|
|
37
|
+
if isinstance(language, str):
|
|
38
|
+
try:
|
|
39
|
+
language = Language(language.lower())
|
|
40
|
+
except ValueError:
|
|
41
|
+
language = detect_language(ir.metadata.get("path", ""))
|
|
42
|
+
|
|
43
|
+
# Get engine's analyzers
|
|
44
|
+
engine_analyzers = self._analyzers.get(engine_name, {})
|
|
45
|
+
|
|
46
|
+
# Try exact language match
|
|
47
|
+
if language in engine_analyzers:
|
|
48
|
+
analyzer = engine_analyzers[language]
|
|
49
|
+
return await self._run_analyzer(analyzer, ir, config)
|
|
50
|
+
|
|
51
|
+
# Try fallback
|
|
52
|
+
if engine_name in self._fallback_analyzers:
|
|
53
|
+
return await self._run_analyzer(self._fallback_analyzers[engine_name], ir, config)
|
|
54
|
+
|
|
55
|
+
# No analyzer available
|
|
56
|
+
caps = get_capabilities(language)
|
|
57
|
+
return EngineResult(
|
|
58
|
+
engine=engine_name,
|
|
59
|
+
findings=[],
|
|
60
|
+
ok=True,
|
|
61
|
+
metrics={"skipped": True, "reason": f"No analyzer for {language.value}", "language": language.value}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def _run_analyzer(self, analyzer: Any, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
65
|
+
"""Run analyzer with error handling."""
|
|
66
|
+
try:
|
|
67
|
+
if asyncio.iscoroutinefunction(analyzer.run):
|
|
68
|
+
return await analyzer.run(ir, config)
|
|
69
|
+
else:
|
|
70
|
+
return analyzer.run(ir, config)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
return EngineResult(
|
|
73
|
+
engine=analyzer.__class__.__name__,
|
|
74
|
+
findings=[],
|
|
75
|
+
errors=[str(e)],
|
|
76
|
+
ok=False
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async def analyze_batch(self, engine_name: str, irs: List[OrionIR], config: EngineConfig) -> EngineResult:
|
|
80
|
+
"""Analyze multiple IRs with the appropriate language analyzers."""
|
|
81
|
+
all_findings = []
|
|
82
|
+
all_errors = []
|
|
83
|
+
engines_run = 0
|
|
84
|
+
languages_seen = set()
|
|
85
|
+
|
|
86
|
+
for ir in irs:
|
|
87
|
+
language = ir.metadata.get("language")
|
|
88
|
+
if isinstance(language, str):
|
|
89
|
+
try:
|
|
90
|
+
language = Language(language.lower())
|
|
91
|
+
except ValueError:
|
|
92
|
+
language = detect_language(ir.metadata.get("path", ""))
|
|
93
|
+
languages_seen.add(language)
|
|
94
|
+
|
|
95
|
+
# Group by language for efficiency
|
|
96
|
+
language_to_irs: Dict[Language, List[OrionIR]] = {}
|
|
97
|
+
for ir in irs:
|
|
98
|
+
lang = ir.metadata.get("language")
|
|
99
|
+
if isinstance(lang, str):
|
|
100
|
+
try:
|
|
101
|
+
lang = Language(lang.lower())
|
|
102
|
+
except ValueError:
|
|
103
|
+
lang = detect_language(ir.metadata.get("path", ""))
|
|
104
|
+
if lang not in language_to_irs:
|
|
105
|
+
language_to_irs[lang] = []
|
|
106
|
+
language_to_irs[lang].append(ir)
|
|
107
|
+
|
|
108
|
+
# Run per language
|
|
109
|
+
for language, lang_irs in language_to_irs.items():
|
|
110
|
+
result = await self.analyze(engine_name, lang_irs[0], config)
|
|
111
|
+
# For batch, we'd need the analyzer to support batch
|
|
112
|
+
# For now, run individually
|
|
113
|
+
for ir in lang_irs:
|
|
114
|
+
r = await self.analyze(engine_name, ir, config)
|
|
115
|
+
if r.findings:
|
|
116
|
+
all_findings.extend(r.findings)
|
|
117
|
+
if r.errors:
|
|
118
|
+
all_errors.extend(r.errors)
|
|
119
|
+
engines_run += 1
|
|
120
|
+
|
|
121
|
+
return EngineResult(
|
|
122
|
+
engine=engine_name,
|
|
123
|
+
findings=all_findings,
|
|
124
|
+
errors=all_errors,
|
|
125
|
+
ok=len(all_errors) == 0,
|
|
126
|
+
metrics={"engines_run": engines_run, "languages": [l.value for l in languages_seen]}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Language-specific regex patterns (fallback when no AST)
|
|
131
|
+
UNIVERSAL_PATTERNS = {
|
|
132
|
+
# Secrets - universal regex
|
|
133
|
+
"secrets": [
|
|
134
|
+
(re.compile(r'AKIA[0-9A-Z]{16}'), "aws_access_key", "AWS Access Key", 0.95),
|
|
135
|
+
(re.compile(r'gh[pousr]_[A-Za-z0-9_]{36}'), "github_token", "GitHub Token", 0.95),
|
|
136
|
+
(re.compile(r'sk_live_[a-zA-Z0-9]{24}'), "stripe_secret", "Stripe Secret Key", 0.95),
|
|
137
|
+
(re.compile(r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}'), "jwt_token", "JWT Token", 0.85),
|
|
138
|
+
(re.compile(r'xox[baprs]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32}'), "slack_token", "Slack Token", 0.95),
|
|
139
|
+
(re.compile(r'(?i)(api[_-]?key|secret|token|password|passwd)\s*[:=]\s*[\'"][a-zA-Z0-9_\-]{20,}[\'"]'), "generic_secret", "Generic Secret", 0.75),
|
|
140
|
+
(re.compile(r'-----BEGIN (?:RSA|DSA|EC|OPENSSH) PRIVATE KEY-----'), "private_key", "Private Key", 0.99),
|
|
141
|
+
],
|
|
142
|
+
|
|
143
|
+
# SQL Injection patterns (universal)
|
|
144
|
+
"sql_injection": [
|
|
145
|
+
(re.compile(r'(SELECT|INSERT|UPDATE|DELETE|UNION|DROP|CREATE|ALTER)\s+.*[\+\%].*(input|req\.|request\.|params|query|body)'), "sql_injection", "Potential SQL Injection", 0.8),
|
|
146
|
+
(re.compile(r'execute\s*\(.*[\+\%].*(input|req\.|request\.|params|query|body)'), "sql_injection", "Potential SQL Injection", 0.85),
|
|
147
|
+
(re.compile(r'query\s*\(.*[\+\%].*(input|req\.|request\.|params|query|body)'), "sql_injection", "Potential SQL Injection", 0.85),
|
|
148
|
+
],
|
|
149
|
+
|
|
150
|
+
# Command Injection (universal)
|
|
151
|
+
"command_injection": [
|
|
152
|
+
(re.compile(r'(system|exec|popen|subprocess|ProcessBuilder|Runtime\.exec|os\.exec|child_process)\s*\(.*[\+\%].*(input|req\.|request\.|params|query|body|argv)'), "command_injection", "Potential Command Injection", 0.85),
|
|
153
|
+
(re.compile(r'(shell\s*=\s*True|shell_exec|passthru|proc_open|`.*\$\{).*input'), "command_injection", "Potential Command Injection", 0.9),
|
|
154
|
+
],
|
|
155
|
+
|
|
156
|
+
# XSS (universal)
|
|
157
|
+
"xss": [
|
|
158
|
+
(re.compile(r'innerHTML\s*=\s*.*(req\.|request\.|params|query|body)'), "xss", "Potential XSS via innerHTML", 0.85),
|
|
159
|
+
(re.compile(r'document\.write\s*\(.*(req\.|request\.|params|query|body)'), "xss", "Potential XSS via document.write", 0.85),
|
|
160
|
+
(re.compile(r'dangerouslySetInnerHTML\s*:\s*\{.*req\.'), "xss", "Potential XSS via dangerouslySetInnerHTML", 0.9),
|
|
161
|
+
(re.compile(r'v-html\s*=\s*.*req\.'), "xss", "Potential XSS via v-html", 0.9),
|
|
162
|
+
],
|
|
163
|
+
|
|
164
|
+
# Path Traversal (universal)
|
|
165
|
+
"path_traversal": [
|
|
166
|
+
(re.compile(r'(open|readFile|readFileSync|FileReader|File\.read|Files\.read|readAllBytes|slurp)\s*\(.*\.\./'), "path_traversal", "Potential Path Traversal", 0.85),
|
|
167
|
+
(re.compile(r'(include|require|require_once|import)\s*\(.*\.\./'), "path_traversal", "Potential Path Traversal", 0.8),
|
|
168
|
+
],
|
|
169
|
+
|
|
170
|
+
# Hardcoded credentials (universal)
|
|
171
|
+
"hardcoded_creds": [
|
|
172
|
+
(re.compile(r'(?i)(password|passwd|pwd|secret|api_?key|token|access_?key)\s*[:=]\s*[\'"][a-zA-Z0-9_\-]{8,}[\'"]'), "hardcoded_secret", "Hardcoded Credential", 0.75),
|
|
173
|
+
(re.compile(r'(?i)(aws_?secret|aws_?key|ssh_?key|private_?key)\s*[:=]\s*[\'"][A-Za-z0-9/\+=]{20,}[\'"]'), "cloud_credential", "Cloud Credential", 0.9),
|
|
174
|
+
],
|
|
175
|
+
|
|
176
|
+
# Weak crypto (universal)
|
|
177
|
+
"weak_crypto": [
|
|
178
|
+
(re.compile(r'(MD5|SHA1|DES|RC4|ECB)\s*\('), "weak_crypto", "Weak Cryptographic Algorithm", 0.85),
|
|
179
|
+
(re.compile(r'(md5|sha1)\s*\(.*password'), "weak_password_hash", "Weak Password Hash", 0.9),
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class UniversalRegexAnalyzer:
|
|
185
|
+
"""Fallback analyzer using universal regex patterns for any language."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, pattern_categories: List[str] = None):
|
|
188
|
+
self.categories = pattern_categories or list(UNIVERSAL_PATTERNS.keys())
|
|
189
|
+
self._compile_patterns()
|
|
190
|
+
|
|
191
|
+
def _compile_patterns(self):
|
|
192
|
+
self.compiled = {}
|
|
193
|
+
for cat in self.categories:
|
|
194
|
+
self.compiled[cat] = [(re.compile(p[0]), p[1], p[2], p[3]) for p in UNIVERSAL_PATTERNS[cat]]
|
|
195
|
+
|
|
196
|
+
def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
197
|
+
try:
|
|
198
|
+
content = ir.content.decode("utf-8", errors="replace")
|
|
199
|
+
except Exception:
|
|
200
|
+
content = str(ir.content)
|
|
201
|
+
|
|
202
|
+
findings = []
|
|
203
|
+
filepath = ir.metadata.get("path", "unknown")
|
|
204
|
+
|
|
205
|
+
for cat, patterns in self.compiled.items():
|
|
206
|
+
for regex, name, desc, conf in patterns:
|
|
207
|
+
for match in regex.finditer(content):
|
|
208
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
209
|
+
lines = content.splitlines()
|
|
210
|
+
start = max(0, line_num - 3)
|
|
211
|
+
end = min(len(lines), line_num + 2)
|
|
212
|
+
snippet = '\n'.join(lines[start:end])
|
|
213
|
+
|
|
214
|
+
findings.append(Finding(
|
|
215
|
+
engine="universal",
|
|
216
|
+
severity=self._severity_from_conf(conf),
|
|
217
|
+
title=f"{cat}: {name}",
|
|
218
|
+
description=desc,
|
|
219
|
+
location=f"{filepath}:{line_num}",
|
|
220
|
+
metadata={
|
|
221
|
+
"pattern": name,
|
|
222
|
+
"confidence": conf,
|
|
223
|
+
"match": match.group()[:100],
|
|
224
|
+
"snippet": snippet,
|
|
225
|
+
"file": filepath,
|
|
226
|
+
"line": line_num,
|
|
227
|
+
"category": cat,
|
|
228
|
+
}
|
|
229
|
+
))
|
|
230
|
+
|
|
231
|
+
return EngineResult(engine="universal", findings=findings, ok=True)
|
|
232
|
+
|
|
233
|
+
def _severity_from_conf(self, conf: float) -> str:
|
|
234
|
+
if conf >= 0.9: return "critical"
|
|
235
|
+
if conf >= 0.8: return "high"
|
|
236
|
+
if conf >= 0.7: return "medium"
|
|
237
|
+
if conf >= 0.5: return "low"
|
|
238
|
+
return "info"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# Polyglot engine base class
|
|
242
|
+
class PolyglotEngine:
|
|
243
|
+
"""Base class for engines that support multiple languages."""
|
|
244
|
+
|
|
245
|
+
name = "polyglot_base"
|
|
246
|
+
version = "1.0.0"
|
|
247
|
+
|
|
248
|
+
def __init__(self):
|
|
249
|
+
self.universal = UniversalRegexAnalyzer()
|
|
250
|
+
self.language_analyzers: Dict[Language, Any] = {}
|
|
251
|
+
|
|
252
|
+
def register_language_analyzer(self, language: Language, analyzer: Any):
|
|
253
|
+
"""Register a language-specific analyzer."""
|
|
254
|
+
self.language_analyzers[language] = analyzer
|
|
255
|
+
|
|
256
|
+
async def check_availability(self) -> bool:
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
def resource_limits(self):
|
|
260
|
+
from orion.engine.config import ResourceLimits
|
|
261
|
+
return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512)
|
|
262
|
+
|
|
263
|
+
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
264
|
+
"""Route to language-specific analyzer or universal fallback."""
|
|
265
|
+
language = ir.metadata.get("language")
|
|
266
|
+
if isinstance(language, str):
|
|
267
|
+
try:
|
|
268
|
+
language = Language(language.lower())
|
|
269
|
+
except ValueError:
|
|
270
|
+
from orion.engine.languages import detect_language
|
|
271
|
+
language = detect_language(ir.metadata.get("path", ""))
|
|
272
|
+
|
|
273
|
+
# Try language-specific analyzer
|
|
274
|
+
if language in self.language_analyzers:
|
|
275
|
+
analyzer = self.language_analyzers[language]
|
|
276
|
+
if asyncio.iscoroutinefunction(analyzer.run):
|
|
277
|
+
return await analyzer.run(ir, config)
|
|
278
|
+
else:
|
|
279
|
+
return analyzer.run(ir, config)
|
|
280
|
+
|
|
281
|
+
# Fallback to universal regex
|
|
282
|
+
return self.universal.run(ir, config)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Example: Polyglot SAST Engine
|
|
286
|
+
class PolyglotSASTEngine(PolyglotEngine):
|
|
287
|
+
name = "sast"
|
|
288
|
+
version = "2.0.0"
|
|
289
|
+
|
|
290
|
+
def __init__(self):
|
|
291
|
+
super().__init__()
|
|
292
|
+
# Python AST analyzer (existing)
|
|
293
|
+
from orion.engine.sast.patterns import run_ast_patterns
|
|
294
|
+
self._python_ast = run_ast_patterns
|
|
295
|
+
|
|
296
|
+
async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
297
|
+
language = ir.metadata.get("language")
|
|
298
|
+
if isinstance(language, str):
|
|
299
|
+
try:
|
|
300
|
+
language = Language(language.lower())
|
|
301
|
+
except ValueError:
|
|
302
|
+
from orion.engine.languages import detect_language
|
|
303
|
+
language = detect_language(ir.metadata.get("path", ""))
|
|
304
|
+
|
|
305
|
+
if language == Language.PYTHON:
|
|
306
|
+
return await self._run_python_ast(ir, config)
|
|
307
|
+
|
|
308
|
+
# For other languages, use universal regex
|
|
309
|
+
return self.universal.run(ir, config)
|
|
310
|
+
|
|
311
|
+
async def _run_python_ast(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
|
|
312
|
+
try:
|
|
313
|
+
content = ir.content.decode("utf-8", errors="replace")
|
|
314
|
+
except Exception:
|
|
315
|
+
content = str(ir.content)
|
|
316
|
+
|
|
317
|
+
filepath = ir.metadata.get("path", "unknown")
|
|
318
|
+
ast_findings = self._python_ast(content, filepath)
|
|
319
|
+
|
|
320
|
+
findings = []
|
|
321
|
+
for af in ast_findings:
|
|
322
|
+
findings.append(Finding(
|
|
323
|
+
engine=self.name,
|
|
324
|
+
severity=af.get('severity', 'medium').lower(),
|
|
325
|
+
title=f"{af['pattern']}: {af['description']}",
|
|
326
|
+
description=f"{af['description']} (CWE: {af['cwe']}, CVSS: {af['cvss']})",
|
|
327
|
+
location=f"{filepath}:{af['line']}",
|
|
328
|
+
metadata={
|
|
329
|
+
"pattern": af['pattern'],
|
|
330
|
+
"cwe": af['cwe'],
|
|
331
|
+
"cvss": af['cvss'],
|
|
332
|
+
"confidence": af['confidence'],
|
|
333
|
+
"snippet": af['snippet'],
|
|
334
|
+
"file": filepath,
|
|
335
|
+
"line": af['line'],
|
|
336
|
+
}
|
|
337
|
+
))
|
|
338
|
+
|
|
339
|
+
return EngineResult(engine=self.name, findings=findings, ok=True)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# Export key classes
|
|
343
|
+
__all__ = [
|
|
344
|
+
"PolyglotDispatcher",
|
|
345
|
+
"PolyglotEngine",
|
|
346
|
+
"PolyglotSASTEngine",
|
|
347
|
+
"UniversalRegexAnalyzer",
|
|
348
|
+
"UNIVERSAL_PATTERNS",
|
|
349
|
+
"Language",
|
|
350
|
+
]
|