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,291 @@
1
+ """Maven/Gradle dependency analyzer for Java projects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import xml.etree.ElementTree as ET
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional
9
+
10
+ from cve_sentinel.analyzers.base import (
11
+ AnalyzerRegistry,
12
+ BaseAnalyzer,
13
+ FileDetector,
14
+ Package,
15
+ )
16
+
17
+
18
+ class MavenAnalyzer(BaseAnalyzer):
19
+ """Analyzer for Maven pom.xml files.
20
+
21
+ Supports:
22
+ - pom.xml (Level 1: direct dependencies)
23
+ """
24
+
25
+ # Maven POM namespace
26
+ POM_NS = "{http://maven.apache.org/POM/4.0.0}"
27
+
28
+ @property
29
+ def ecosystem(self) -> str:
30
+ """Return the ecosystem name."""
31
+ return "maven"
32
+
33
+ @property
34
+ def manifest_patterns(self) -> List[str]:
35
+ """Return glob patterns for manifest files."""
36
+ default_patterns = ["pom.xml"]
37
+ custom = self._custom_patterns.get("manifests", [])
38
+ return default_patterns + custom
39
+
40
+ @property
41
+ def lock_patterns(self) -> List[str]:
42
+ """Return glob patterns for lock files."""
43
+ default_patterns: List[str] = [] # Maven doesn't have a standard lock file
44
+ custom = self._custom_patterns.get("locks", [])
45
+ return default_patterns + custom
46
+
47
+ def __init__(
48
+ self,
49
+ analysis_level: int = 1,
50
+ custom_patterns: Optional[Dict[str, List[str]]] = None,
51
+ ) -> None:
52
+ """Initialize Maven analyzer.
53
+
54
+ Args:
55
+ analysis_level: Analysis depth (1=manifest only)
56
+ custom_patterns: Optional custom file patterns {"manifests": [...], "locks": [...]}
57
+ """
58
+ self.analysis_level = analysis_level
59
+ self._custom_patterns = custom_patterns or {}
60
+ self._file_detector = FileDetector()
61
+
62
+ def detect_files(self, path: Path) -> List[Path]:
63
+ """Detect Maven dependency files."""
64
+ return self._file_detector.find_files(path, self.manifest_patterns)
65
+
66
+ def parse(self, file_path: Path) -> List[Package]:
67
+ """Parse a Maven pom.xml file."""
68
+ if file_path.name == "pom.xml":
69
+ return self._parse_pom_xml(file_path)
70
+ return []
71
+
72
+ def _parse_pom_xml(self, file_path: Path) -> List[Package]:
73
+ """Parse pom.xml file."""
74
+ packages: List[Package] = []
75
+ content = file_path.read_text(encoding="utf-8")
76
+
77
+ try:
78
+ # Try parsing with namespace
79
+ root = ET.fromstring(content)
80
+ except ET.ParseError:
81
+ return packages
82
+
83
+ # Detect namespace
84
+ ns = ""
85
+ if root.tag.startswith("{"):
86
+ ns = root.tag.split("}")[0] + "}"
87
+
88
+ # Extract properties for variable substitution
89
+ properties = self._extract_properties(root, ns)
90
+
91
+ # Find dependencies section
92
+ deps_section = root.find(f"{ns}dependencies")
93
+ if deps_section is None:
94
+ return packages
95
+
96
+ for dep in deps_section.findall(f"{ns}dependency"):
97
+ group_id = self._get_text(dep, f"{ns}groupId")
98
+ artifact_id = self._get_text(dep, f"{ns}artifactId")
99
+ version = self._get_text(dep, f"{ns}version")
100
+ scope = self._get_text(dep, f"{ns}scope")
101
+
102
+ if not group_id or not artifact_id:
103
+ continue
104
+
105
+ # Resolve property references
106
+ if version:
107
+ version = self._resolve_properties(version, properties)
108
+ else:
109
+ version = "*" # Version might be managed by parent POM
110
+
111
+ # Skip test scope by default
112
+ if scope == "test":
113
+ continue
114
+
115
+ # Format as groupId:artifactId
116
+ name = f"{group_id}:{artifact_id}"
117
+
118
+ packages.append(
119
+ Package(
120
+ name=name,
121
+ version=version,
122
+ ecosystem=self.ecosystem,
123
+ source_file=file_path,
124
+ source_line=None,
125
+ is_direct=True,
126
+ )
127
+ )
128
+
129
+ return packages
130
+
131
+ def _extract_properties(self, root: ET.Element, ns: str) -> Dict[str, str]:
132
+ """Extract properties from POM."""
133
+ properties: Dict[str, str] = {}
134
+ props_section = root.find(f"{ns}properties")
135
+ if props_section is not None:
136
+ for prop in props_section:
137
+ # Remove namespace from tag
138
+ tag = prop.tag.replace(ns, "")
139
+ if prop.text:
140
+ properties[tag] = prop.text
141
+ return properties
142
+
143
+ def _resolve_properties(self, value: str, properties: Dict[str, str]) -> str:
144
+ """Resolve ${property} references in value."""
145
+ pattern = r"\$\{([^}]+)\}"
146
+ matches = re.findall(pattern, value)
147
+ for match in matches:
148
+ if match in properties:
149
+ value = value.replace(f"${{{match}}}", properties[match])
150
+ return value
151
+
152
+ def _get_text(self, element: ET.Element, path: str) -> Optional[str]:
153
+ """Get text content of a child element."""
154
+ child = element.find(path)
155
+ if child is not None and child.text:
156
+ return child.text.strip()
157
+ return None
158
+
159
+
160
+ class GradleAnalyzer(BaseAnalyzer):
161
+ """Analyzer for Gradle build files.
162
+
163
+ Supports:
164
+ - build.gradle (Level 1: direct dependencies)
165
+ - build.gradle.kts (Level 1: Kotlin DSL)
166
+ """
167
+
168
+ @property
169
+ def ecosystem(self) -> str:
170
+ """Return the ecosystem name."""
171
+ return "maven" # Gradle uses Maven repositories
172
+
173
+ @property
174
+ def manifest_patterns(self) -> List[str]:
175
+ """Return glob patterns for manifest files."""
176
+ default_patterns = ["build.gradle", "build.gradle.kts"]
177
+ custom = self._custom_patterns.get("manifests", [])
178
+ return default_patterns + custom
179
+
180
+ @property
181
+ def lock_patterns(self) -> List[str]:
182
+ """Return glob patterns for lock files."""
183
+ default_patterns: List[str] = []
184
+ custom = self._custom_patterns.get("locks", [])
185
+ return default_patterns + custom
186
+
187
+ def __init__(
188
+ self,
189
+ analysis_level: int = 1,
190
+ custom_patterns: Optional[Dict[str, List[str]]] = None,
191
+ ) -> None:
192
+ """Initialize Gradle analyzer.
193
+
194
+ Args:
195
+ analysis_level: Analysis depth (1=manifest only)
196
+ custom_patterns: Optional custom file patterns {"manifests": [...], "locks": [...]}
197
+ """
198
+ self.analysis_level = analysis_level
199
+ self._custom_patterns = custom_patterns or {}
200
+ self._file_detector = FileDetector()
201
+
202
+ def detect_files(self, path: Path) -> List[Path]:
203
+ """Detect Gradle build files."""
204
+ return self._file_detector.find_files(path, self.manifest_patterns)
205
+
206
+ def parse(self, file_path: Path) -> List[Package]:
207
+ """Parse a Gradle build file."""
208
+ if file_path.name in ("build.gradle", "build.gradle.kts"):
209
+ return self._parse_build_gradle(file_path)
210
+ return []
211
+
212
+ def _parse_build_gradle(self, file_path: Path) -> List[Package]:
213
+ """Parse build.gradle or build.gradle.kts file."""
214
+ packages: List[Package] = []
215
+ content = file_path.read_text(encoding="utf-8")
216
+ lines = content.split("\n")
217
+
218
+ in_dependencies = False
219
+ brace_count = 0
220
+
221
+ for line_num, line in enumerate(lines, start=1):
222
+ stripped = line.strip()
223
+
224
+ # Track dependencies block
225
+ if re.match(r"dependencies\s*\{", stripped):
226
+ in_dependencies = True
227
+ brace_count = 1
228
+ continue
229
+
230
+ if in_dependencies:
231
+ brace_count += stripped.count("{") - stripped.count("}")
232
+ if brace_count <= 0:
233
+ in_dependencies = False
234
+ continue
235
+
236
+ # Parse dependency declarations
237
+ # Groovy: implementation 'group:artifact:version'
238
+ # Kotlin: implementation("group:artifact:version")
239
+ patterns = [
240
+ # Groovy string
241
+ r"(implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompile)\s+['\"]([^'\"]+)['\"]",
242
+ # Kotlin function
243
+ r"(implementation|api|compile|compileOnly|runtimeOnly|testImplementation|testCompile)\s*\(\s*['\"]([^'\"]+)['\"]",
244
+ # Groovy map style
245
+ r"(implementation|api|compile)\s+group:\s*['\"]([^'\"]+)['\"],\s*name:\s*['\"]([^'\"]+)['\"],\s*version:\s*['\"]([^'\"]+)['\"]",
246
+ ]
247
+
248
+ for pattern in patterns:
249
+ match = re.search(pattern, stripped)
250
+ if match:
251
+ groups = match.groups()
252
+ config_type = groups[0]
253
+
254
+ # Skip test dependencies
255
+ if "test" in config_type.lower():
256
+ continue
257
+
258
+ if len(groups) == 4:
259
+ # Map style: group, name, version
260
+ name = f"{groups[1]}:{groups[2]}"
261
+ version = groups[3]
262
+ else:
263
+ # String style: group:artifact:version
264
+ dep_str = groups[1]
265
+ parts = dep_str.split(":")
266
+ if len(parts) >= 2:
267
+ name = f"{parts[0]}:{parts[1]}"
268
+ version = parts[2] if len(parts) > 2 else "*"
269
+ else:
270
+ continue
271
+
272
+ packages.append(
273
+ Package(
274
+ name=name,
275
+ version=version,
276
+ ecosystem=self.ecosystem,
277
+ source_file=file_path,
278
+ source_line=line_num,
279
+ is_direct=True,
280
+ )
281
+ )
282
+ break
283
+
284
+ return packages
285
+
286
+
287
+ def register() -> None:
288
+ """Register Maven and Gradle analyzers."""
289
+ registry = AnalyzerRegistry.get_instance()
290
+ registry.register(MavenAnalyzer())
291
+ registry.register(GradleAnalyzer())