@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.
Files changed (129) hide show
  1. package/README.md +80 -190
  2. package/bin/orion.js +30 -29
  3. package/dist/orion.exe +0 -0
  4. package/package.json +46 -48
  5. package/docs/API_REFERENCE.md +0 -211
  6. package/docs/ARCHITECTURE.md +0 -183
  7. package/docs/CHANGELOG.md +0 -112
  8. package/docs/CONTRIBUTING.md +0 -157
  9. package/docs/ENGINE_CATALOG.md +0 -87
  10. package/docs/GETTING_STARTED.md +0 -226
  11. package/pyproject.toml +0 -16
  12. package/src/orion/__init__.py +0 -0
  13. package/src/orion/__pycache__/__init__.cpython-313.pyc +0 -0
  14. package/src/orion/engine/__init__.py +0 -17
  15. package/src/orion/engine/__pycache__/__init__.cpython-313.pyc +0 -0
  16. package/src/orion/engine/__pycache__/config.cpython-313.pyc +0 -0
  17. package/src/orion/engine/__pycache__/core.cpython-313.pyc +0 -0
  18. package/src/orion/engine/__pycache__/ir.cpython-313.pyc +0 -0
  19. package/src/orion/engine/__pycache__/languages.cpython-313.pyc +0 -0
  20. package/src/orion/engine/__pycache__/registry.cpython-313.pyc +0 -0
  21. package/src/orion/engine/__pycache__/results.cpython-313.pyc +0 -0
  22. package/src/orion/engine/__pycache__/runner.cpython-313.pyc +0 -0
  23. package/src/orion/engine/api_security/__pycache__/engine.cpython-313.pyc +0 -0
  24. package/src/orion/engine/api_security/engine.py +0 -125
  25. package/src/orion/engine/architecture/__pycache__/engine.cpython-313.pyc +0 -0
  26. package/src/orion/engine/architecture/engine.py +0 -463
  27. package/src/orion/engine/authorization/__pycache__/engine.cpython-313.pyc +0 -0
  28. package/src/orion/engine/authorization/engine.py +0 -337
  29. package/src/orion/engine/callgraph/__pycache__/engine.cpython-313.pyc +0 -0
  30. package/src/orion/engine/callgraph/engine.py +0 -236
  31. package/src/orion/engine/cfg/__pycache__/engine.cpython-313.pyc +0 -0
  32. package/src/orion/engine/cfg/engine.py +0 -509
  33. package/src/orion/engine/chaos/__pycache__/engine.cpython-313.pyc +0 -0
  34. package/src/orion/engine/chaos/engine.py +0 -268
  35. package/src/orion/engine/chaos_config/__pycache__/engine.cpython-313.pyc +0 -0
  36. package/src/orion/engine/chaos_config/engine.py +0 -322
  37. package/src/orion/engine/complexity/__pycache__/engine.cpython-313.pyc +0 -0
  38. package/src/orion/engine/complexity/engine.py +0 -144
  39. package/src/orion/engine/compliance/__pycache__/engine.cpython-313.pyc +0 -0
  40. package/src/orion/engine/compliance/engine.py +0 -325
  41. package/src/orion/engine/config.py +0 -41
  42. package/src/orion/engine/config_engine/__pycache__/engine.cpython-313.pyc +0 -0
  43. package/src/orion/engine/config_engine/engine.py +0 -364
  44. package/src/orion/engine/config_validation/__pycache__/engine.cpython-313.pyc +0 -0
  45. package/src/orion/engine/config_validation/engine.py +0 -313
  46. package/src/orion/engine/container/__pycache__/engine.cpython-313.pyc +0 -0
  47. package/src/orion/engine/container/engine.py +0 -546
  48. package/src/orion/engine/contract_probe/__pycache__/engine.cpython-313.pyc +0 -0
  49. package/src/orion/engine/contract_probe/engine.py +0 -316
  50. package/src/orion/engine/core.py +0 -75
  51. package/src/orion/engine/crypto_guard/__pycache__/engine.cpython-313.pyc +0 -0
  52. package/src/orion/engine/crypto_guard/engine.py +0 -134
  53. package/src/orion/engine/data_residency/__pycache__/engine.cpython-313.pyc +0 -0
  54. package/src/orion/engine/data_residency/engine.py +0 -238
  55. package/src/orion/engine/dataflow/__pycache__/engine.cpython-313.pyc +0 -0
  56. package/src/orion/engine/dataflow/engine.py +0 -398
  57. package/src/orion/engine/dead_import/__pycache__/engine.cpython-313.pyc +0 -0
  58. package/src/orion/engine/dead_import/engine.py +0 -293
  59. package/src/orion/engine/deps/__pycache__/engine.cpython-313.pyc +0 -0
  60. package/src/orion/engine/deps/engine.py +0 -244
  61. package/src/orion/engine/dispatcher.py +0 -350
  62. package/src/orion/engine/docs/AUDIT_PROMPT.md +0 -1248
  63. package/src/orion/engine/duplication/__pycache__/engine.cpython-313.pyc +0 -0
  64. package/src/orion/engine/duplication/engine.py +0 -120
  65. package/src/orion/engine/error_handling/__pycache__/engine.cpython-313.pyc +0 -0
  66. package/src/orion/engine/error_handling/engine.py +0 -256
  67. package/src/orion/engine/fp_learn/__pycache__/engine.cpython-313.pyc +0 -0
  68. package/src/orion/engine/fp_learn/engine.py +0 -248
  69. package/src/orion/engine/hunter/__pycache__/engine.cpython-313.pyc +0 -0
  70. package/src/orion/engine/hunter/engine.py +0 -324
  71. package/src/orion/engine/iac_security/__pycache__/engine.cpython-313.pyc +0 -0
  72. package/src/orion/engine/iac_security/engine.py +0 -218
  73. package/src/orion/engine/ir.py +0 -22
  74. package/src/orion/engine/languages.py +0 -454
  75. package/src/orion/engine/license/__pycache__/engine.cpython-313.pyc +0 -0
  76. package/src/orion/engine/license/engine.py +0 -172
  77. package/src/orion/engine/lint/__pycache__/engine.cpython-313.pyc +0 -0
  78. package/src/orion/engine/lint/engine.py +0 -558
  79. package/src/orion/engine/llm_security/__pycache__/engine.cpython-313.pyc +0 -0
  80. package/src/orion/engine/llm_security/engine.py +0 -125
  81. package/src/orion/engine/logging_c/__pycache__/engine.cpython-313.pyc +0 -0
  82. package/src/orion/engine/logging_c/engine.py +0 -164
  83. package/src/orion/engine/noise/__pycache__/engine.cpython-313.pyc +0 -0
  84. package/src/orion/engine/noise/engine.py +0 -140
  85. package/src/orion/engine/orphan/__pycache__/engine.cpython-313.pyc +0 -0
  86. package/src/orion/engine/orphan/engine.py +0 -244
  87. package/src/orion/engine/perf_guard/__pycache__/engine.cpython-313.pyc +0 -0
  88. package/src/orion/engine/perf_guard/engine.py +0 -345
  89. package/src/orion/engine/prompt_security/__pycache__/engine.cpython-313.pyc +0 -0
  90. package/src/orion/engine/prompt_security/detectors/__init__.py +0 -4
  91. package/src/orion/engine/prompt_security/engine.py +0 -170
  92. package/src/orion/engine/provenance/__pycache__/engine.cpython-313.pyc +0 -0
  93. package/src/orion/engine/provenance/engine.py +0 -242
  94. package/src/orion/engine/registry.py +0 -55
  95. package/src/orion/engine/regression/__pycache__/engine.cpython-313.pyc +0 -0
  96. package/src/orion/engine/regression/engine.py +0 -213
  97. package/src/orion/engine/results.py +0 -21
  98. package/src/orion/engine/runner.py +0 -1310
  99. package/src/orion/engine/sandbox_escape/__pycache__/engine.cpython-313.pyc +0 -0
  100. package/src/orion/engine/sandbox_escape/engine.py +0 -284
  101. package/src/orion/engine/sast/__pycache__/engine.cpython-313.pyc +0 -0
  102. package/src/orion/engine/sast/engine.py +0 -217
  103. package/src/orion/engine/sast/patterns/__init__.py +0 -513
  104. package/src/orion/engine/sast/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  105. package/src/orion/engine/secrets/__pycache__/engine.cpython-313.pyc +0 -0
  106. package/src/orion/engine/secrets/engine.py +0 -77
  107. package/src/orion/engine/secrets/patterns/__init__.py +0 -74
  108. package/src/orion/engine/secrets/patterns/__pycache__/__init__.cpython-313.pyc +0 -0
  109. package/src/orion/engine/sensitive_data/__pycache__/engine.cpython-313.pyc +0 -0
  110. package/src/orion/engine/sensitive_data/engine.py +0 -143
  111. package/src/orion/engine/state/__pycache__/engine.cpython-313.pyc +0 -0
  112. package/src/orion/engine/state/engine.py +0 -218
  113. package/src/orion/engine/supply_chain_sbom/__pycache__/engine.cpython-313.pyc +0 -0
  114. package/src/orion/engine/supply_chain_sbom/engine.py +0 -290
  115. package/src/orion/engine/symbol/__pycache__/engine.cpython-313.pyc +0 -0
  116. package/src/orion/engine/symbol/engine.py +0 -463
  117. package/src/orion/engine/taint/__pycache__/engine.cpython-313.pyc +0 -0
  118. package/src/orion/engine/taint/engine.py +0 -464
  119. package/src/orion/engine/test_coverage/__pycache__/engine.cpython-313.pyc +0 -0
  120. package/src/orion/engine/test_coverage/engine.py +0 -445
  121. package/src/orion/engine/threat_model/__pycache__/engine.cpython-313.pyc +0 -0
  122. package/src/orion/engine/threat_model/engine.py +0 -126
  123. package/src/orion/engine/wiring_trace/__pycache__/engine.cpython-313.pyc +0 -0
  124. package/src/orion/engine/wiring_trace/engine.py +0 -200
  125. package/src/orion_engine.egg-info/PKG-INFO +0 -7
  126. package/src/orion_engine.egg-info/SOURCES.txt +0 -65
  127. package/src/orion_engine.egg-info/dependency_links.txt +0 -1
  128. package/src/orion_engine.egg-info/entry_points.txt +0 -2
  129. package/src/orion_engine.egg-info/top_level.txt +0 -1
@@ -1,293 +0,0 @@
1
- """Dead import detection engine - finds unused imports using AST analysis."""
2
- import ast
3
- from typing import Dict, List, Set, Optional, Any
4
- from dataclasses import dataclass, field
5
- from collections import defaultdict
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
- @dataclass
12
- class ImportInfo:
13
- name: str
14
- alias: Optional[str]
15
- module: str
16
- lineno: int
17
- is_from_import: bool
18
- is_future: bool = False
19
- is_side_effect: bool = False
20
- used: bool = False
21
- used_as: Set[str] = field(default_factory=set)
22
-
23
-
24
- class ImportVisitor(ast.NodeVisitor):
25
- def __init__(self, filename: str, source_lines: List[str]):
26
- self.filename = filename
27
- self.source_lines = source_lines
28
- self.findings: List[Finding] = []
29
- self.imports: List[ImportInfo] = []
30
- self.used_names: Set[str] = set()
31
- self.used_attributes: Dict[str, Set[str]] = defaultdict(set)
32
- self.scope_stack: List[Set[str]] = [set()]
33
- self.future_imports: Set[str] = set()
34
- self.current_class: Optional[str] = None
35
-
36
- def get_snippet(self, node: ast.AST, context: int = 2) -> str:
37
- start = max(0, node.lineno - 1 - context)
38
- end = min(len(self.source_lines),
39
- getattr(node, 'end_lineno', node.lineno) + context if hasattr(node, 'end_lineno') else node.lineno + context)
40
- return "\n".join(self.source_lines[start:end])
41
-
42
- def current_scope(self) -> Set[str]:
43
- return self.scope_stack[-1]
44
-
45
- def enter_scope(self):
46
- self.scope_stack.append(set())
47
-
48
- def exit_scope(self):
49
- if len(self.scope_stack) > 1:
50
- self.scope_stack.pop()
51
-
52
- def mark_used(self, name: str):
53
- self.used_names.add(name)
54
- for scope in self.scope_stack:
55
- scope.add(name)
56
-
57
- def visit_Import(self, node: ast.Import):
58
- for alias in node.names:
59
- name = alias.asname or alias.name
60
- module = alias.name
61
-
62
- is_future = module == "__future__"
63
- is_side_effect = is_future or module in (
64
- "typing", "typing_extensions", "types", "dataclasses",
65
- "contextlib", "warnings", "logging", "sys", "os", "builtins"
66
- )
67
-
68
- info = ImportInfo(
69
- name=name,
70
- alias=alias.asname,
71
- module=module,
72
- lineno=node.lineno,
73
- is_from_import=False,
74
- is_future=is_future,
75
- is_side_effect=is_side_effect,
76
- )
77
- self.imports.append(info)
78
-
79
- if not is_future:
80
- self.current_scope().add(name)
81
-
82
- self.generic_visit(node)
83
-
84
- def visit_ImportFrom(self, node: ast.ImportFrom):
85
- if node.module:
86
- is_future = node.module == "__future__"
87
- is_init_side_effect = node.module.endswith(".__init__") or (
88
- node.level > 0 and not node.module
89
- )
90
-
91
- for alias in node.names:
92
- name = alias.asname or alias.name
93
- full_module = f"{node.module}.{alias.name}" if node.module else alias.name
94
-
95
- is_side_effect = is_future or is_init_side_effect or (
96
- node.module in ("typing", "typing_extensions", "types", "dataclasses")
97
- )
98
-
99
- info = ImportInfo(
100
- name=name,
101
- alias=alias.asname,
102
- module=full_module,
103
- lineno=node.lineno,
104
- is_from_import=True,
105
- is_future=is_future,
106
- is_side_effect=is_side_effect,
107
- )
108
- self.imports.append(info)
109
-
110
- if not is_future:
111
- self.current_scope().add(name)
112
-
113
- self.generic_visit(node)
114
-
115
- def visit_Name(self, node: ast.Name):
116
- if isinstance(node.ctx, ast.Load):
117
- self.mark_used(node.id)
118
- self.generic_visit(node)
119
-
120
- def visit_Attribute(self, node: ast.Attribute):
121
- if isinstance(node.ctx, ast.Load):
122
- if isinstance(node.value, ast.Name):
123
- base = node.value.id
124
- self.used_attributes[base].add(node.attr)
125
- self.mark_used(base)
126
- self.generic_visit(node)
127
-
128
- def visit_FunctionDef(self, node: ast.FunctionDef):
129
- self.enter_scope()
130
- for arg in node.args.args:
131
- self.current_scope().add(arg.arg)
132
- if node.args.vararg:
133
- self.current_scope().add(node.args.vararg.arg)
134
- if node.args.kwarg:
135
- self.current_scope().add(node.args.kwarg.arg)
136
- self.generic_visit(node)
137
- self.exit_scope()
138
-
139
- def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
140
- self.visit_FunctionDef(node)
141
-
142
- def visit_ClassDef(self, node: ast.ClassDef):
143
- old_class = self.current_class
144
- self.current_class = node.name
145
- self.enter_scope()
146
- self.generic_visit(node)
147
- self.exit_scope()
148
- self.current_class = old_class
149
-
150
- def visit_Lambda(self, node: ast.Lambda):
151
- self.enter_scope()
152
- for arg in node.args.args:
153
- self.current_scope().add(arg.arg)
154
- self.generic_visit(node)
155
- self.exit_scope()
156
-
157
- def visit_Comprehension(self, node: ast.comprehension):
158
- if isinstance(node.target, ast.Name):
159
- self.current_scope().add(node.target.id)
160
- elif isinstance(node.target, (ast.Tuple, ast.List)):
161
- for elt in node.target.elts:
162
- if isinstance(elt, ast.Name):
163
- self.current_scope().add(elt.id)
164
- self.generic_visit(node)
165
-
166
- def visit_Global(self, node: ast.Global):
167
- for name in node.names:
168
- self.mark_used(name)
169
-
170
- def visit_Nonlocal(self, node: ast.Nonlocal):
171
- for name in node.names:
172
- self.mark_used(name)
173
-
174
- def visit_ExceptHandler(self, node: ast.ExceptHandler):
175
- if node.name:
176
- self.current_scope().add(node.name)
177
- self.generic_visit(node)
178
-
179
- def visit_With(self, node: ast.With):
180
- for item in node.items:
181
- if item.optional_vars:
182
- if isinstance(item.optional_vars, ast.Name):
183
- self.current_scope().add(item.optional_vars.id)
184
- elif isinstance(item.optional_vars, (ast.Tuple, ast.List)):
185
- for elt in item.optional_vars.elts:
186
- if isinstance(elt, ast.Name):
187
- self.current_scope().add(elt.id)
188
- self.generic_visit(node)
189
-
190
- def visit_Match(self, node: ast.Match):
191
- self.enter_scope()
192
- self.generic_visit(node)
193
- self.exit_scope()
194
-
195
- def visit_match_case(self, node: ast.match_case):
196
- self.enter_scope()
197
- self.generic_visit(node.pattern)
198
- self.generic_visit(node.guard)
199
- for stmt in node.body:
200
- self.generic_visit(stmt)
201
- self.exit_scope()
202
-
203
-
204
- class Engine:
205
- name = "dead_import"
206
- version = "1.0.0"
207
-
208
- async def check_availability(self) -> bool:
209
- return True
210
-
211
- def resource_limits(self) -> ResourceLimits:
212
- return ResourceLimits(max_cpu_seconds=60, max_memory_mb=512, max_files=50000)
213
-
214
- async def run(self, ctx, config: EngineConfig) -> EngineResult:
215
- # Handle both single IR and list of IRs
216
- if not isinstance(ctx, list):
217
- ctx = [ctx]
218
- all_findings = []
219
- errors = []
220
- files_processed = 0
221
-
222
- exclude_patterns = config.toggles.get("exclude", []) if config.toggles else []
223
-
224
- for ir in ctx:
225
- try:
226
- if not ir.metadata.get("language") == "python":
227
- continue
228
-
229
- rel_path = ir.metadata.get("path", "unknown.py")
230
- if any(pattern in rel_path for pattern in exclude_patterns):
231
- continue
232
-
233
- content = ir.content.decode("utf-8", errors="replace")
234
- source_lines = content.splitlines()
235
- tree = ast.parse(content, filename=rel_path)
236
-
237
- visitor = ImportVisitor(rel_path, source_lines)
238
- visitor.visit(tree)
239
-
240
- for imp in visitor.imports:
241
- if imp.is_future or imp.is_side_effect:
242
- continue
243
-
244
- used_name = imp.alias or imp.name
245
-
246
- is_used = (
247
- used_name in visitor.used_names or
248
- used_name in visitor.used_attributes or
249
- any(used_name in scope for scope in visitor.scope_stack)
250
- )
251
-
252
- if not is_used:
253
- for attr in visitor.used_attributes.get(used_name, []):
254
- is_used = True
255
- break
256
-
257
- if not is_used:
258
- all_findings.append(Finding(
259
- engine="dead_import",
260
- severity="low",
261
- title="Unused import",
262
- description=f"Import '{imp.name}' from '{imp.module}' is never used",
263
- location=f"{rel_path}:{imp.lineno}",
264
- metadata={
265
- "snippet": visitor.get_snippet_by_line(imp.lineno),
266
- "cwe": "CWE-1041",
267
- "confidence": 0.9,
268
- "imported": imp.name,
269
- "module": imp.module,
270
- "alias": imp.alias,
271
- "is_from_import": imp.is_from_import,
272
- }
273
- ))
274
-
275
- files_processed += 1
276
-
277
- except SyntaxError as e:
278
- errors.append(f"Syntax error in {ir.metadata.get('path', 'unknown')}: {e}")
279
- except Exception as e:
280
- errors.append(f"Error processing {ir.metadata.get('path', 'unknown')}: {e}")
281
-
282
- return EngineResult(
283
- engine=self.name,
284
- findings=all_findings,
285
- errors=errors,
286
- metrics={"files_processed": files_processed, "findings_count": len(all_findings)},
287
- ok=len(errors) == 0
288
- )
289
-
290
- def get_snippet_by_line(self, lines: List[str], line: int, context: int = 2) -> str:
291
- start = max(0, line - 1 - context)
292
- end = min(len(lines), line + context)
293
- return "\n".join(lines[start:end])
@@ -1,244 +0,0 @@
1
- """Dependency vulnerability scanner engine."""
2
- import re
3
- import json
4
- import tomllib
5
- from pathlib import Path
6
- from orion.engine.config import EngineConfig
7
- from orion.engine.ir import OrionIR
8
- from orion.engine.results import EngineResult, Finding
9
-
10
-
11
- class Engine:
12
- name = "deps"
13
- version = "1.0.0"
14
-
15
- VULN_DB = {
16
- "requests": [("2.28.0", "CVE-2022-32891", "2.25.1")],
17
- "urllib3": [("1.26.5", "CVE-2021-33503", "1.26.4")],
18
- "django": [("4.1.7", "CVE-2022-34265", "4.1.6")],
19
- "flask": [("2.2.2", "CVE-2022-29242", "2.2.1")],
20
- "spring-boot": [("2.7.8", "CVE-2022-22965", "2.7.7")],
21
- "log4j": [("2.17.1", "CVE-2021-44228", "2.17.0")],
22
- "jackson-databind": [("2.13.2", "CVE-2020-36518", "2.13.1")],
23
- }
24
-
25
- GPL_LICENSES = {"GPL-2.0", "GPL-3.0", "AGPL-3.0", "LGPL-2.1", "LGPL-3.0"}
26
-
27
- async def check_availability(self) -> bool:
28
- return True
29
-
30
- def resource_limits(self):
31
- from orion.engine.config import ResourceLimits
32
- return ResourceLimits(max_cpu_seconds=60, max_memory_mb=256)
33
-
34
- async def run(self, ir: OrionIR, config: EngineConfig) -> EngineResult:
35
- try:
36
- text = ir.content.decode("utf-8", errors="replace")
37
- except Exception:
38
- text = str(ir.content)
39
-
40
- path = ir.metadata.get("path", "unknown")
41
- filename = Path(path).name.lower()
42
- findings = []
43
- deps = {}
44
-
45
- if filename in ("requirements.txt",) or filename.startswith("requirements-") and filename.endswith(".txt"):
46
- deps = self._parse_requirements(text)
47
- elif filename == "pyproject.toml":
48
- deps = self._parse_pyproject_toml(text)
49
- elif filename == "package.json":
50
- deps = self._parse_package_json(text)
51
- elif filename == "pipfile":
52
- deps = self._parse_pipfile(text)
53
- elif filename in ("poetry.lock", "pnpm-lock.yaml", "yarn.lock"):
54
- deps = self._parse_lock_file(text, filename)
55
-
56
- if not deps:
57
- return EngineResult(engine=self.name, findings=findings, ok=True)
58
-
59
- for name, version in deps.items():
60
- findings.extend(self._check_vulnerabilities(name, version, path))
61
- findings.extend(self._check_license_conflicts(name, version, path))
62
- findings.extend(self._check_outdated(name, version, path))
63
-
64
- if filename in ("requirements.txt", "package.json", "pipfile", "pyproject.toml"):
65
- if not self._has_lock_file(path):
66
- findings.append(Finding(
67
- engine=self.name,
68
- severity="info",
69
- title="Missing lock file",
70
- description="No lock file found for dependency pinning",
71
- location=path,
72
- metadata={"confidence": 0.9, "file": filename, "cwe": "CWE-1104"},
73
- ))
74
-
75
- return EngineResult(engine=self.name, findings=findings, ok=True)
76
-
77
- def _parse_requirements(self, text: str) -> dict:
78
- deps = {}
79
- for line in text.splitlines():
80
- line = line.strip()
81
- if not line or line.startswith("#") or line.startswith("-"):
82
- continue
83
- match = re.match(r"^([A-Za-z0-9_\-\.]+)(?:[=<>!~]+([A-Za-z0-9_\-\.]+))?", line)
84
- if match:
85
- name = match.group(1).lower()
86
- version = match.group(2) or "unknown"
87
- deps[name] = version
88
- return deps
89
-
90
- def _parse_pyproject_toml(self, text: str) -> dict:
91
- deps = {}
92
- try:
93
- data = tomllib.loads(text)
94
- for section in ("project", "tool.poetry"):
95
- if section in data:
96
- dep_section = data[section].get("dependencies", {})
97
- for name, spec in dep_section.items():
98
- if name.lower() != "python":
99
- version = spec if isinstance(spec, str) else spec.get("version", "unknown")
100
- deps[name.lower()] = version
101
- except Exception:
102
- pass
103
- return deps
104
-
105
- def _parse_package_json(self, text: str) -> dict:
106
- deps = {}
107
- try:
108
- data = json.loads(text)
109
- for section in ("dependencies", "devDependencies"):
110
- for name, version in data.get(section, {}).items():
111
- deps[name.lower()] = version.lstrip("^~>=")
112
- except Exception:
113
- pass
114
- return deps
115
-
116
- def _parse_pipfile(self, text: str) -> dict:
117
- deps = {}
118
- current_section = None
119
- for line in text.splitlines():
120
- line = line.strip()
121
- if line.startswith("[") and line.endswith("]"):
122
- current_section = line[1:-1].lower()
123
- continue
124
- if current_section in ("packages", "dev-packages") and "=" in line:
125
- parts = line.split("=", 1)
126
- name = parts[0].strip().lower()
127
- version = parts[1].strip().strip('"\'')
128
- deps[name] = version
129
- return deps
130
-
131
- def _parse_lock_file(self, text: str, filename: str) -> dict:
132
- deps = {}
133
- try:
134
- if filename == "poetry.lock":
135
- data = tomllib.loads(text)
136
- for pkg in data.get("package", []):
137
- deps[pkg["name"].lower()] = pkg["version"]
138
- elif filename == "pnpm-lock.yaml":
139
- for line in text.splitlines():
140
- match = re.match(r"^\s+([^:]+):\s+\"([^\"]+)\"", line)
141
- if match and not match.group(1).startswith(" "):
142
- deps[match.group(1).lower()] = match.group(2)
143
- elif filename == "yarn.lock":
144
- for line in text.splitlines():
145
- match = re.match(r'^"([^@]+)@[^"]+":', line)
146
- if match:
147
- name = match.group(1).lower()
148
- version_match = re.search(r'version\s+"([^"]+)"', line)
149
- if version_match:
150
- deps[name] = version_match.group(1)
151
- except Exception:
152
- pass
153
- return deps
154
-
155
- def _check_vulnerabilities(self, name: str, version: str, path: str):
156
- findings = []
157
- name_lower = name.lower()
158
- if name_lower in self.VULN_DB and version != "unknown":
159
- for fixed_version, cve, affected_version in self.VULN_DB[name_lower]:
160
- if self._version_less_than(version, fixed_version):
161
- findings.append(Finding(
162
- engine=self.name,
163
- severity="high",
164
- title=f"Vulnerable dependency: {name}",
165
- description=f"{name} {version} is vulnerable to {cve}",
166
- location=path,
167
- metadata={
168
- "confidence": 0.9,
169
- "package": name,
170
- "version": version,
171
- "vuln": cve,
172
- "fixed_in": fixed_version,
173
- "cwe": "CWE-1104",
174
- "cvss": 7.5,
175
- },
176
- ))
177
- return findings
178
-
179
- def _check_license_conflicts(self, name: str, version: str, path: str):
180
- findings = []
181
- if name_lower in {"python-gnupg", "pygpl", "gpl-package"}:
182
- findings.append(Finding(
183
- engine=self.name,
184
- severity="medium",
185
- title=f"GPL/AGPL dependency detected: {name}",
186
- description=f"Dependency {name} uses GPL/AGPL license which may conflict with project license",
187
- location=path,
188
- metadata={
189
- "confidence": 0.75,
190
- "package": name,
191
- "license": "GPL-3.0",
192
- "cwe": "CWE-1104",
193
- },
194
- ))
195
- return findings
196
-
197
- def _check_outdated(self, name: str, version: str, path: str):
198
- findings = []
199
- major_match = re.match(r"^(\d+)\.", version)
200
- if major_match:
201
- major = int(major_match.group(1))
202
- if major >= 2:
203
- findings.append(Finding(
204
- engine=self.name,
205
- severity="low",
206
- title=f"Potentially outdated dependency: {name}",
207
- description=f"{name} {version} may be outdated (major version >= 2, consider checking for updates)",
208
- location=path,
209
- metadata={
210
- "confidence": 0.6,
211
- "package": name,
212
- "version": version,
213
- "note": "Offline heuristic - verify manually",
214
- },
215
- ))
216
- return findings
217
-
218
- def _has_lock_file(self, path: str) -> bool:
219
- p = Path(path)
220
- parent = p.parent
221
- lock_files = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "Pipfile.lock"]
222
- return any((parent / lf).exists() for lf in lock_files)
223
-
224
- def _version_less_than(self, v1: str, v2: str) -> bool:
225
- try:
226
- parts1 = [int(x) for x in re.split(r"[.\-]", v1) if x.isdigit()]
227
- parts2 = [int(x) for x in re.split(r"[.\-]", v2) if x.isdigit()]
228
- for i in range(max(len(parts1), len(parts2))):
229
- p1 = parts1[i] if i < len(parts1) else 0
230
- p2 = parts2[i] if i < len(parts2) else 0
231
- if p1 < p2:
232
- return True
233
- elif p1 > p2:
234
- return False
235
- return False
236
- except Exception:
237
- return False
238
-
239
-
240
- _name_lower = None # placeholder to avoid unused warning in _check_vulnerabilities
241
-
242
-
243
- def _version_key(v):
244
- return tuple(int(x) for x in re.split(r"[.\-]", v) if x.isdigit())