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.
- cve_sentinel/__init__.py +4 -0
- cve_sentinel/__main__.py +18 -0
- cve_sentinel/analyzers/__init__.py +19 -0
- cve_sentinel/analyzers/base.py +274 -0
- cve_sentinel/analyzers/go.py +186 -0
- cve_sentinel/analyzers/maven.py +291 -0
- cve_sentinel/analyzers/npm.py +586 -0
- cve_sentinel/analyzers/php.py +238 -0
- cve_sentinel/analyzers/python.py +435 -0
- cve_sentinel/analyzers/ruby.py +182 -0
- cve_sentinel/analyzers/rust.py +199 -0
- cve_sentinel/cli.py +517 -0
- cve_sentinel/config.py +347 -0
- cve_sentinel/fetchers/__init__.py +22 -0
- cve_sentinel/fetchers/nvd.py +544 -0
- cve_sentinel/fetchers/osv.py +719 -0
- cve_sentinel/matcher.py +496 -0
- cve_sentinel/reporter.py +549 -0
- cve_sentinel/scanner.py +513 -0
- cve_sentinel/scanners/__init__.py +13 -0
- cve_sentinel/scanners/import_scanner.py +1121 -0
- cve_sentinel/utils/__init__.py +5 -0
- cve_sentinel/utils/cache.py +61 -0
- cve_sentinel-0.1.2.dist-info/METADATA +454 -0
- cve_sentinel-0.1.2.dist-info/RECORD +28 -0
- cve_sentinel-0.1.2.dist-info/WHEEL +4 -0
- cve_sentinel-0.1.2.dist-info/entry_points.txt +2 -0
- cve_sentinel-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -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())
|