cve-sentinel 0.1.2__py3-none-any.whl

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.
@@ -0,0 +1,182 @@
1
+ """Ruby dependency analyzer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Dict, List, Optional
8
+
9
+ from cve_sentinel.analyzers.base import (
10
+ AnalyzerRegistry,
11
+ BaseAnalyzer,
12
+ FileDetector,
13
+ Package,
14
+ )
15
+
16
+
17
+ class RubyAnalyzer(BaseAnalyzer):
18
+ """Analyzer for Ruby Bundler.
19
+
20
+ Supports:
21
+ - Gemfile (Level 1: direct dependencies)
22
+ - Gemfile.lock (Level 2: transitive dependencies)
23
+ """
24
+
25
+ @property
26
+ def ecosystem(self) -> str:
27
+ """Return the ecosystem name."""
28
+ return "rubygems"
29
+
30
+ @property
31
+ def manifest_patterns(self) -> List[str]:
32
+ """Return glob patterns for manifest files."""
33
+ default_patterns = ["Gemfile"]
34
+ custom = self._custom_patterns.get("manifests", [])
35
+ return default_patterns + custom
36
+
37
+ @property
38
+ def lock_patterns(self) -> List[str]:
39
+ """Return glob patterns for lock files."""
40
+ default_patterns = ["Gemfile.lock"]
41
+ custom = self._custom_patterns.get("locks", [])
42
+ return default_patterns + custom
43
+
44
+ def __init__(
45
+ self,
46
+ analysis_level: int = 2,
47
+ custom_patterns: Optional[Dict[str, List[str]]] = None,
48
+ ) -> None:
49
+ """Initialize Ruby analyzer.
50
+
51
+ Args:
52
+ analysis_level: Analysis depth (1=manifest only, 2=include lock files)
53
+ custom_patterns: Optional custom file patterns {"manifests": [...], "locks": [...]}
54
+ """
55
+ self.analysis_level = analysis_level
56
+ self._custom_patterns = custom_patterns or {}
57
+ self._file_detector = FileDetector()
58
+
59
+ def detect_files(self, path: Path) -> List[Path]:
60
+ """Detect Ruby dependency files."""
61
+ patterns = self.manifest_patterns.copy()
62
+ if self.analysis_level >= 2:
63
+ patterns.extend(self.lock_patterns)
64
+ return self._file_detector.find_files(path, patterns)
65
+
66
+ def parse(self, file_path: Path) -> List[Package]:
67
+ """Parse a Ruby dependency file."""
68
+ if file_path.name == "Gemfile":
69
+ return self._parse_gemfile(file_path)
70
+ elif file_path.name == "Gemfile.lock":
71
+ return self._parse_gemfile_lock(file_path)
72
+ return []
73
+
74
+ def _parse_gemfile(self, file_path: Path) -> List[Package]:
75
+ """Parse Gemfile."""
76
+ packages: List[Package] = []
77
+ content = file_path.read_text(encoding="utf-8")
78
+ lines = content.split("\n")
79
+
80
+ for line_num, line in enumerate(lines, start=1):
81
+ stripped = line.strip()
82
+
83
+ # Skip comments and empty lines
84
+ if not stripped or stripped.startswith("#"):
85
+ continue
86
+
87
+ # Parse gem declarations
88
+ # gem 'name', 'version'
89
+ # gem 'name', '~> version'
90
+ # gem 'name', '>= version', '< version'
91
+ # gem 'name', version: 'x.y.z'
92
+ match = re.match(r"gem\s+['\"]([^'\"]+)['\"](?:\s*,\s*(.+))?", stripped)
93
+ if match:
94
+ name = match.group(1)
95
+ version_part = match.group(2)
96
+
97
+ version = self._extract_version(version_part) if version_part else "*"
98
+
99
+ packages.append(
100
+ Package(
101
+ name=name,
102
+ version=version,
103
+ ecosystem=self.ecosystem,
104
+ source_file=file_path,
105
+ source_line=line_num,
106
+ is_direct=True,
107
+ )
108
+ )
109
+
110
+ return packages
111
+
112
+ def _extract_version(self, version_str: str) -> str:
113
+ """Extract version from Gemfile version specification."""
114
+ if not version_str:
115
+ return "*"
116
+
117
+ # Handle version: 'x.y.z' syntax
118
+ version_match = re.search(r"version:\s*['\"]([^'\"]+)['\"]", version_str)
119
+ if version_match:
120
+ return self._normalize_version(version_match.group(1))
121
+
122
+ # Handle string version: '~> 1.2.3' or '>= 1.0'
123
+ string_match = re.search(r"['\"]([^'\"]+)['\"]", version_str)
124
+ if string_match:
125
+ return self._normalize_version(string_match.group(1))
126
+
127
+ return "*"
128
+
129
+ def _normalize_version(self, version: str) -> str:
130
+ """Normalize Ruby version specifier."""
131
+ # Remove operators: ~>, >=, <=, >, <, =
132
+ version = re.sub(r"^[~>=<]+\s*", "", version.strip())
133
+ return version
134
+
135
+ def _parse_gemfile_lock(self, file_path: Path) -> List[Package]:
136
+ """Parse Gemfile.lock."""
137
+ packages: List[Package] = []
138
+ content = file_path.read_text(encoding="utf-8")
139
+ lines = content.split("\n")
140
+
141
+ in_specs = False
142
+ seen: set = set()
143
+
144
+ for line in lines:
145
+ # Check for GEM section specs
146
+ if line.strip() == "specs:":
147
+ in_specs = True
148
+ continue
149
+
150
+ # End of specs section (new section or blank line after indented content)
151
+ if in_specs and line and not line.startswith(" "):
152
+ in_specs = False
153
+ continue
154
+
155
+ if in_specs:
156
+ # Parse gem entries: " gem_name (version)"
157
+ # Indentation of 4 spaces indicates a direct spec
158
+ match = re.match(r"^ (\S+)\s+\(([^)]+)\)$", line)
159
+ if match:
160
+ name = match.group(1)
161
+ version = match.group(2)
162
+
163
+ pkg_key = (name, version)
164
+ if pkg_key not in seen:
165
+ seen.add(pkg_key)
166
+ packages.append(
167
+ Package(
168
+ name=name,
169
+ version=version,
170
+ ecosystem=self.ecosystem,
171
+ source_file=file_path,
172
+ source_line=None,
173
+ is_direct=False,
174
+ )
175
+ )
176
+
177
+ return packages
178
+
179
+
180
+ def register() -> None:
181
+ """Register the Ruby analyzer."""
182
+ AnalyzerRegistry.get_instance().register(RubyAnalyzer())
@@ -0,0 +1,199 @@
1
+ """Rust dependency analyzer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ if sys.version_info >= (3, 11):
11
+ import tomllib
12
+ else:
13
+ import tomli as tomllib
14
+
15
+ from cve_sentinel.analyzers.base import (
16
+ AnalyzerRegistry,
17
+ BaseAnalyzer,
18
+ FileDetector,
19
+ Package,
20
+ )
21
+
22
+
23
+ class RustAnalyzer(BaseAnalyzer):
24
+ """Analyzer for Rust Cargo.
25
+
26
+ Supports:
27
+ - Cargo.toml (Level 1: direct dependencies)
28
+ - Cargo.lock (Level 2: transitive dependencies)
29
+ """
30
+
31
+ @property
32
+ def ecosystem(self) -> str:
33
+ """Return the ecosystem name."""
34
+ return "crates.io"
35
+
36
+ @property
37
+ def manifest_patterns(self) -> List[str]:
38
+ """Return glob patterns for manifest files."""
39
+ default_patterns = ["Cargo.toml"]
40
+ custom = self._custom_patterns.get("manifests", [])
41
+ return default_patterns + custom
42
+
43
+ @property
44
+ def lock_patterns(self) -> List[str]:
45
+ """Return glob patterns for lock files."""
46
+ default_patterns = ["Cargo.lock"]
47
+ custom = self._custom_patterns.get("locks", [])
48
+ return default_patterns + custom
49
+
50
+ def __init__(
51
+ self,
52
+ analysis_level: int = 2,
53
+ custom_patterns: Optional[Dict[str, List[str]]] = None,
54
+ ) -> None:
55
+ """Initialize Rust analyzer.
56
+
57
+ Args:
58
+ analysis_level: Analysis depth (1=manifest only, 2=include lock files)
59
+ custom_patterns: Optional custom file patterns {"manifests": [...], "locks": [...]}
60
+ """
61
+ self.analysis_level = analysis_level
62
+ self._custom_patterns = custom_patterns or {}
63
+ self._file_detector = FileDetector()
64
+
65
+ def detect_files(self, path: Path) -> List[Path]:
66
+ """Detect Rust dependency files."""
67
+ patterns = self.manifest_patterns.copy()
68
+ if self.analysis_level >= 2:
69
+ patterns.extend(self.lock_patterns)
70
+ return self._file_detector.find_files(path, patterns)
71
+
72
+ def parse(self, file_path: Path) -> List[Package]:
73
+ """Parse a Rust dependency file."""
74
+ if file_path.name == "Cargo.toml":
75
+ return self._parse_cargo_toml(file_path)
76
+ elif file_path.name == "Cargo.lock":
77
+ return self._parse_cargo_lock(file_path)
78
+ return []
79
+
80
+ def _parse_cargo_toml(self, file_path: Path) -> List[Package]:
81
+ """Parse Cargo.toml file."""
82
+ packages: List[Package] = []
83
+ content = file_path.read_text(encoding="utf-8")
84
+
85
+ try:
86
+ data = tomllib.loads(content)
87
+ except Exception:
88
+ return packages
89
+
90
+ # Get package name to skip self-references
91
+ pkg_name = data.get("package", {}).get("name", "")
92
+
93
+ # Parse dependencies
94
+ deps = data.get("dependencies", {})
95
+ packages.extend(self._parse_deps_section(deps, file_path, pkg_name, True))
96
+
97
+ # Parse dev-dependencies
98
+ dev_deps = data.get("dev-dependencies", {})
99
+ packages.extend(self._parse_deps_section(dev_deps, file_path, pkg_name, True))
100
+
101
+ # Parse build-dependencies
102
+ build_deps = data.get("build-dependencies", {})
103
+ packages.extend(self._parse_deps_section(build_deps, file_path, pkg_name, True))
104
+
105
+ return packages
106
+
107
+ def _parse_deps_section(
108
+ self,
109
+ deps: Dict[str, Any],
110
+ file_path: Path,
111
+ skip_name: str,
112
+ is_direct: bool,
113
+ ) -> List[Package]:
114
+ """Parse a dependencies section."""
115
+ packages: List[Package] = []
116
+
117
+ for name, spec in deps.items():
118
+ if name == skip_name:
119
+ continue
120
+
121
+ version = self._extract_version(spec)
122
+ if not version:
123
+ continue
124
+
125
+ packages.append(
126
+ Package(
127
+ name=name,
128
+ version=version,
129
+ ecosystem=self.ecosystem,
130
+ source_file=file_path,
131
+ source_line=None,
132
+ is_direct=is_direct,
133
+ )
134
+ )
135
+
136
+ return packages
137
+
138
+ def _extract_version(self, spec: Any) -> Optional[str]:
139
+ """Extract version from dependency specification."""
140
+ if isinstance(spec, str):
141
+ return self._normalize_version(spec)
142
+ elif isinstance(spec, dict):
143
+ version = spec.get("version")
144
+ if version:
145
+ return self._normalize_version(version)
146
+ # Git or path dependencies don't have versions
147
+ if spec.get("git") or spec.get("path"):
148
+ return None
149
+ return None
150
+
151
+ def _normalize_version(self, version: str) -> str:
152
+ """Normalize Cargo version specifier."""
153
+ # Remove operators: ^, ~, >=, <=, >, <, =
154
+ version = re.sub(r"^[\^~>=<]+\s*", "", version.strip())
155
+ # Handle wildcard: 1.* -> 1.0
156
+ version = re.sub(r"\.\*$", ".0", version)
157
+ return version
158
+
159
+ def _parse_cargo_lock(self, file_path: Path) -> List[Package]:
160
+ """Parse Cargo.lock file."""
161
+ packages: List[Package] = []
162
+ content = file_path.read_text(encoding="utf-8")
163
+
164
+ try:
165
+ data = tomllib.loads(content)
166
+ except Exception:
167
+ return packages
168
+
169
+ # Parse [[package]] entries
170
+ pkg_list = data.get("package", [])
171
+ seen: set = set()
172
+
173
+ for pkg in pkg_list:
174
+ name = pkg.get("name", "")
175
+ version = pkg.get("version", "")
176
+
177
+ if not name or not version:
178
+ continue
179
+
180
+ pkg_key = (name, version)
181
+ if pkg_key not in seen:
182
+ seen.add(pkg_key)
183
+ packages.append(
184
+ Package(
185
+ name=name,
186
+ version=version,
187
+ ecosystem=self.ecosystem,
188
+ source_file=file_path,
189
+ source_line=None,
190
+ is_direct=False,
191
+ )
192
+ )
193
+
194
+ return packages
195
+
196
+
197
+ def register() -> None:
198
+ """Register the Rust analyzer."""
199
+ AnalyzerRegistry.get_instance().register(RustAnalyzer())