security-use 0.1.1__py3-none-any.whl → 0.2.9__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.
- security_use/__init__.py +9 -1
- security_use/auth/__init__.py +16 -0
- security_use/auth/client.py +223 -0
- security_use/auth/config.py +177 -0
- security_use/auth/oauth.py +317 -0
- security_use/cli.py +699 -34
- security_use/compliance/__init__.py +10 -0
- security_use/compliance/mapper.py +275 -0
- security_use/compliance/models.py +50 -0
- security_use/dependency_scanner.py +76 -30
- security_use/fixers/iac_fixer.py +173 -95
- security_use/iac/rules/azure.py +246 -0
- security_use/iac/rules/gcp.py +255 -0
- security_use/iac/rules/kubernetes.py +429 -0
- security_use/iac/rules/registry.py +56 -0
- security_use/parsers/__init__.py +18 -0
- security_use/parsers/base.py +2 -0
- security_use/parsers/composer.py +101 -0
- security_use/parsers/conda.py +97 -0
- security_use/parsers/dotnet.py +89 -0
- security_use/parsers/gradle.py +90 -0
- security_use/parsers/maven.py +108 -0
- security_use/parsers/npm.py +196 -0
- security_use/parsers/yarn.py +108 -0
- security_use/reporter.py +29 -1
- security_use/sbom/__init__.py +10 -0
- security_use/sbom/generator.py +340 -0
- security_use/sbom/models.py +40 -0
- security_use/scanner.py +15 -2
- security_use/sensor/__init__.py +125 -0
- security_use/sensor/alert_queue.py +207 -0
- security_use/sensor/config.py +217 -0
- security_use/sensor/dashboard_alerter.py +246 -0
- security_use/sensor/detector.py +415 -0
- security_use/sensor/endpoint_analyzer.py +339 -0
- security_use/sensor/middleware.py +521 -0
- security_use/sensor/models.py +140 -0
- security_use/sensor/webhook.py +227 -0
- security_use-0.2.9.dist-info/METADATA +531 -0
- security_use-0.2.9.dist-info/RECORD +60 -0
- security_use-0.2.9.dist-info/licenses/LICENSE +21 -0
- security_use-0.1.1.dist-info/METADATA +0 -92
- security_use-0.1.1.dist-info/RECORD +0 -30
- {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/WHEEL +0 -0
- {security_use-0.1.1.dist-info → security_use-0.2.9.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Analyze application endpoints to identify vulnerable paths.
|
|
2
|
+
|
|
3
|
+
This module combines dependency scanning with code analysis to identify
|
|
4
|
+
which API endpoints use vulnerable packages and should be monitored.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class EndpointInfo:
|
|
19
|
+
"""Information about a discovered endpoint."""
|
|
20
|
+
|
|
21
|
+
path: str
|
|
22
|
+
method: str = "GET"
|
|
23
|
+
function_name: str = ""
|
|
24
|
+
file_path: str = ""
|
|
25
|
+
line_number: int = 0
|
|
26
|
+
imports: list[str] = field(default_factory=list)
|
|
27
|
+
vulnerable_packages: list[str] = field(default_factory=list)
|
|
28
|
+
risk_score: float = 0.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class AnalysisResult:
|
|
33
|
+
"""Result of endpoint vulnerability analysis."""
|
|
34
|
+
|
|
35
|
+
all_endpoints: list[EndpointInfo] = field(default_factory=list)
|
|
36
|
+
vulnerable_endpoints: list[EndpointInfo] = field(default_factory=list)
|
|
37
|
+
vulnerable_paths: list[str] = field(default_factory=list)
|
|
38
|
+
vulnerable_packages: dict[str, list[str]] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VulnerableEndpointDetector:
|
|
42
|
+
"""Detect which endpoints use vulnerable packages.
|
|
43
|
+
|
|
44
|
+
Combines dependency scanning with static code analysis to identify
|
|
45
|
+
API routes that should be monitored more closely.
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
from security_use.sensor import VulnerableEndpointDetector
|
|
49
|
+
|
|
50
|
+
detector = VulnerableEndpointDetector()
|
|
51
|
+
result = detector.analyze("./my-project")
|
|
52
|
+
|
|
53
|
+
# Get paths to monitor
|
|
54
|
+
vulnerable_paths = result.vulnerable_paths
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Patterns for detecting route decorators
|
|
58
|
+
FASTAPI_ROUTE_PATTERNS = [
|
|
59
|
+
r'@(?:app|router)\.(?:get|post|put|delete|patch|options|head)\s*\(\s*["\']([^"\']+)["\']',
|
|
60
|
+
r'@(?:app|router)\.api_route\s*\(\s*["\']([^"\']+)["\']',
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
FLASK_ROUTE_PATTERNS = [
|
|
64
|
+
r'@(?:app|bp|blueprint)\.route\s*\(\s*["\']([^"\']+)["\']',
|
|
65
|
+
r'@(?:app|bp|blueprint)\.(?:get|post|put|delete|patch)\s*\(\s*["\']([^"\']+)["\']',
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# High-risk packages that handle user input
|
|
69
|
+
HIGH_RISK_PACKAGES = {
|
|
70
|
+
"flask": ["request", "g", "session"],
|
|
71
|
+
"fastapi": ["Request", "Form", "File", "Body", "Query", "Path"],
|
|
72
|
+
"django": ["request", "HttpRequest"],
|
|
73
|
+
"sqlalchemy": ["text", "execute", "raw"],
|
|
74
|
+
"pymysql": ["cursor", "execute"],
|
|
75
|
+
"psycopg2": ["cursor", "execute"],
|
|
76
|
+
"sqlite3": ["cursor", "execute"],
|
|
77
|
+
"subprocess": ["run", "call", "Popen", "check_output"],
|
|
78
|
+
"os": ["system", "popen", "exec"],
|
|
79
|
+
"pickle": ["loads", "load"],
|
|
80
|
+
"yaml": ["load", "unsafe_load"],
|
|
81
|
+
"eval": [],
|
|
82
|
+
"exec": [],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def __init__(self, project_path: Optional[str] = None):
|
|
86
|
+
"""Initialize the endpoint detector.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
project_path: Path to the project to analyze.
|
|
90
|
+
"""
|
|
91
|
+
self.project_path = Path(project_path) if project_path else None
|
|
92
|
+
self._vulnerable_packages: set[str] = set()
|
|
93
|
+
|
|
94
|
+
def analyze(self, path: Optional[str] = None) -> AnalysisResult:
|
|
95
|
+
"""Analyze a project for vulnerable endpoints.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
path: Path to the project. Uses project_path if not provided.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
AnalysisResult with vulnerable endpoints and paths.
|
|
102
|
+
"""
|
|
103
|
+
project_path = Path(path) if path else self.project_path
|
|
104
|
+
if not project_path:
|
|
105
|
+
raise ValueError("No project path provided")
|
|
106
|
+
|
|
107
|
+
result = AnalysisResult()
|
|
108
|
+
|
|
109
|
+
# Step 1: Run dependency scan to find vulnerable packages
|
|
110
|
+
self._scan_dependencies(project_path, result)
|
|
111
|
+
|
|
112
|
+
# Step 2: Find all Python files
|
|
113
|
+
python_files = self._find_python_files(project_path)
|
|
114
|
+
|
|
115
|
+
# Step 3: Analyze each file for endpoints
|
|
116
|
+
for file_path in python_files:
|
|
117
|
+
try:
|
|
118
|
+
self._analyze_file(file_path, result)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.debug(f"Error analyzing {file_path}: {e}")
|
|
121
|
+
|
|
122
|
+
# Step 4: Calculate risk scores and filter vulnerable endpoints
|
|
123
|
+
self._calculate_risk_scores(result)
|
|
124
|
+
|
|
125
|
+
# Step 5: Extract vulnerable paths
|
|
126
|
+
result.vulnerable_paths = list(set(
|
|
127
|
+
ep.path for ep in result.vulnerable_endpoints
|
|
128
|
+
))
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
def _scan_dependencies(self, project_path: Path, result: AnalysisResult) -> None:
|
|
133
|
+
"""Scan dependencies for vulnerabilities."""
|
|
134
|
+
try:
|
|
135
|
+
from security_use import scan_dependencies
|
|
136
|
+
|
|
137
|
+
scan_result = scan_dependencies(str(project_path))
|
|
138
|
+
|
|
139
|
+
for vuln in scan_result.vulnerabilities:
|
|
140
|
+
package = vuln.package.lower()
|
|
141
|
+
self._vulnerable_packages.add(package)
|
|
142
|
+
|
|
143
|
+
if package not in result.vulnerable_packages:
|
|
144
|
+
result.vulnerable_packages[package] = []
|
|
145
|
+
result.vulnerable_packages[package].append(vuln.cve_id or vuln.title)
|
|
146
|
+
|
|
147
|
+
logger.info(f"Found {len(self._vulnerable_packages)} vulnerable packages")
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.warning(f"Dependency scan failed: {e}")
|
|
151
|
+
|
|
152
|
+
def _find_python_files(self, project_path: Path) -> list[Path]:
|
|
153
|
+
"""Find all Python files in the project."""
|
|
154
|
+
skip_dirs = {
|
|
155
|
+
"node_modules", ".git", ".venv", "venv", "__pycache__",
|
|
156
|
+
".tox", ".pytest_cache", "dist", "build", ".eggs"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
files = []
|
|
160
|
+
for file_path in project_path.rglob("*.py"):
|
|
161
|
+
if not any(skip in file_path.parts for skip in skip_dirs):
|
|
162
|
+
files.append(file_path)
|
|
163
|
+
|
|
164
|
+
return files
|
|
165
|
+
|
|
166
|
+
def _analyze_file(self, file_path: Path, result: AnalysisResult) -> None:
|
|
167
|
+
"""Analyze a Python file for endpoints."""
|
|
168
|
+
try:
|
|
169
|
+
content = file_path.read_text(encoding="utf-8")
|
|
170
|
+
except Exception:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Extract imports
|
|
174
|
+
imports = self._extract_imports(content)
|
|
175
|
+
|
|
176
|
+
# Find route decorators
|
|
177
|
+
endpoints = self._find_routes(content, str(file_path), imports)
|
|
178
|
+
|
|
179
|
+
for endpoint in endpoints:
|
|
180
|
+
endpoint.imports = imports
|
|
181
|
+
result.all_endpoints.append(endpoint)
|
|
182
|
+
|
|
183
|
+
def _extract_imports(self, content: str) -> list[str]:
|
|
184
|
+
"""Extract imported packages from Python code."""
|
|
185
|
+
imports = []
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
tree = ast.parse(content)
|
|
189
|
+
|
|
190
|
+
for node in ast.walk(tree):
|
|
191
|
+
if isinstance(node, ast.Import):
|
|
192
|
+
for alias in node.names:
|
|
193
|
+
imports.append(alias.name.split(".")[0])
|
|
194
|
+
elif isinstance(node, ast.ImportFrom):
|
|
195
|
+
if node.module:
|
|
196
|
+
imports.append(node.module.split(".")[0])
|
|
197
|
+
|
|
198
|
+
except SyntaxError:
|
|
199
|
+
# Fallback to regex
|
|
200
|
+
import_pattern = r'^(?:from\s+(\w+)|import\s+(\w+))'
|
|
201
|
+
for match in re.finditer(import_pattern, content, re.MULTILINE):
|
|
202
|
+
pkg = match.group(1) or match.group(2)
|
|
203
|
+
if pkg:
|
|
204
|
+
imports.append(pkg)
|
|
205
|
+
|
|
206
|
+
return list(set(imports))
|
|
207
|
+
|
|
208
|
+
def _find_routes(
|
|
209
|
+
self, content: str, file_path: str, imports: list[str]
|
|
210
|
+
) -> list[EndpointInfo]:
|
|
211
|
+
"""Find route definitions in Python code."""
|
|
212
|
+
endpoints = []
|
|
213
|
+
lines = content.split("\n")
|
|
214
|
+
|
|
215
|
+
# Combine patterns based on detected framework
|
|
216
|
+
patterns = []
|
|
217
|
+
if any(imp in ["fastapi", "starlette"] for imp in imports):
|
|
218
|
+
patterns.extend(self.FASTAPI_ROUTE_PATTERNS)
|
|
219
|
+
if any(imp in ["flask"] for imp in imports):
|
|
220
|
+
patterns.extend(self.FLASK_ROUTE_PATTERNS)
|
|
221
|
+
|
|
222
|
+
# If no framework detected, try all patterns
|
|
223
|
+
if not patterns:
|
|
224
|
+
patterns = self.FASTAPI_ROUTE_PATTERNS + self.FLASK_ROUTE_PATTERNS
|
|
225
|
+
|
|
226
|
+
for i, line in enumerate(lines):
|
|
227
|
+
for pattern in patterns:
|
|
228
|
+
match = re.search(pattern, line, re.IGNORECASE)
|
|
229
|
+
if match:
|
|
230
|
+
path = match.group(1)
|
|
231
|
+
|
|
232
|
+
# Detect HTTP method
|
|
233
|
+
method = "GET"
|
|
234
|
+
method_match = re.search(r'\.(get|post|put|delete|patch|options|head)\s*\(', line, re.I)
|
|
235
|
+
if method_match:
|
|
236
|
+
method = method_match.group(1).upper()
|
|
237
|
+
|
|
238
|
+
# Find function name (usually on next line or same line)
|
|
239
|
+
func_name = ""
|
|
240
|
+
for j in range(i, min(i + 3, len(lines))):
|
|
241
|
+
func_match = re.search(r'(?:async\s+)?def\s+(\w+)', lines[j])
|
|
242
|
+
if func_match:
|
|
243
|
+
func_name = func_match.group(1)
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
endpoints.append(EndpointInfo(
|
|
247
|
+
path=path,
|
|
248
|
+
method=method,
|
|
249
|
+
function_name=func_name,
|
|
250
|
+
file_path=file_path,
|
|
251
|
+
line_number=i + 1,
|
|
252
|
+
))
|
|
253
|
+
|
|
254
|
+
return endpoints
|
|
255
|
+
|
|
256
|
+
def _calculate_risk_scores(self, result: AnalysisResult) -> None:
|
|
257
|
+
"""Calculate risk scores for endpoints and identify vulnerable ones."""
|
|
258
|
+
for endpoint in result.all_endpoints:
|
|
259
|
+
score = 0.0
|
|
260
|
+
vulnerable_pkgs = []
|
|
261
|
+
|
|
262
|
+
# Check if endpoint uses vulnerable packages
|
|
263
|
+
for imp in endpoint.imports:
|
|
264
|
+
imp_lower = imp.lower()
|
|
265
|
+
|
|
266
|
+
# Direct vulnerable package
|
|
267
|
+
if imp_lower in self._vulnerable_packages:
|
|
268
|
+
score += 0.5
|
|
269
|
+
vulnerable_pkgs.append(imp_lower)
|
|
270
|
+
|
|
271
|
+
# High-risk package
|
|
272
|
+
if imp_lower in self.HIGH_RISK_PACKAGES:
|
|
273
|
+
score += 0.3
|
|
274
|
+
|
|
275
|
+
# Path-based risk factors
|
|
276
|
+
path_lower = endpoint.path.lower()
|
|
277
|
+
if any(term in path_lower for term in ["admin", "auth", "login", "password", "user"]):
|
|
278
|
+
score += 0.2
|
|
279
|
+
if any(term in path_lower for term in ["upload", "file", "download"]):
|
|
280
|
+
score += 0.2
|
|
281
|
+
if any(term in path_lower for term in ["search", "query", "filter"]):
|
|
282
|
+
score += 0.1
|
|
283
|
+
if any(term in path_lower for term in ["exec", "run", "eval", "shell"]):
|
|
284
|
+
score += 0.3
|
|
285
|
+
|
|
286
|
+
# Method-based risk
|
|
287
|
+
if endpoint.method in ["POST", "PUT", "PATCH", "DELETE"]:
|
|
288
|
+
score += 0.1
|
|
289
|
+
|
|
290
|
+
endpoint.risk_score = min(score, 1.0)
|
|
291
|
+
endpoint.vulnerable_packages = vulnerable_pkgs
|
|
292
|
+
|
|
293
|
+
# Mark as vulnerable if score is high enough or uses vulnerable packages
|
|
294
|
+
if score >= 0.3 or vulnerable_pkgs:
|
|
295
|
+
result.vulnerable_endpoints.append(endpoint)
|
|
296
|
+
|
|
297
|
+
def get_watch_paths(
|
|
298
|
+
self,
|
|
299
|
+
path: Optional[str] = None,
|
|
300
|
+
min_risk_score: float = 0.0,
|
|
301
|
+
include_high_risk: bool = True,
|
|
302
|
+
) -> list[str]:
|
|
303
|
+
"""Get list of paths that should be monitored.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
path: Project path to analyze.
|
|
307
|
+
min_risk_score: Minimum risk score for inclusion.
|
|
308
|
+
include_high_risk: Include high-risk paths even without vulnerabilities.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List of URL paths to monitor.
|
|
312
|
+
"""
|
|
313
|
+
result = self.analyze(path)
|
|
314
|
+
|
|
315
|
+
paths = set()
|
|
316
|
+
|
|
317
|
+
for endpoint in result.vulnerable_endpoints:
|
|
318
|
+
if endpoint.risk_score >= min_risk_score:
|
|
319
|
+
paths.add(endpoint.path)
|
|
320
|
+
|
|
321
|
+
if include_high_risk:
|
|
322
|
+
for endpoint in result.all_endpoints:
|
|
323
|
+
if endpoint.risk_score >= 0.5:
|
|
324
|
+
paths.add(endpoint.path)
|
|
325
|
+
|
|
326
|
+
return list(paths)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def detect_vulnerable_endpoints(project_path: str) -> list[str]:
|
|
330
|
+
"""Convenience function to detect vulnerable endpoints.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
project_path: Path to the project to analyze.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
List of vulnerable endpoint paths.
|
|
337
|
+
"""
|
|
338
|
+
detector = VulnerableEndpointDetector()
|
|
339
|
+
return detector.get_watch_paths(project_path)
|