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
cve_sentinel/scanner.py
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Main scanner module for CVE Sentinel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from cve_sentinel.analyzers.base import AnalysisResult, AnalyzerRegistry, Package
|
|
14
|
+
from cve_sentinel.config import Config, ConfigError, load_config
|
|
15
|
+
from cve_sentinel.fetchers.nvd import NVDClient
|
|
16
|
+
from cve_sentinel.fetchers.osv import OSVClient
|
|
17
|
+
from cve_sentinel.matcher import VulnerabilityMatch, VulnerabilityMatcher
|
|
18
|
+
from cve_sentinel.reporter import Reporter, create_reporter
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ScanResult:
|
|
27
|
+
"""Result of a CVE scan.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
success: Whether the scan completed successfully.
|
|
31
|
+
packages_scanned: Number of packages scanned.
|
|
32
|
+
vulnerabilities: List of detected vulnerabilities.
|
|
33
|
+
errors: List of error messages encountered during scan.
|
|
34
|
+
scan_duration: Duration of the scan in seconds.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
success: bool
|
|
38
|
+
packages_scanned: int
|
|
39
|
+
vulnerabilities: List[VulnerabilityMatch] = field(default_factory=list)
|
|
40
|
+
errors: List[str] = field(default_factory=list)
|
|
41
|
+
scan_duration: float = 0.0
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def has_vulnerabilities(self) -> bool:
|
|
45
|
+
"""Check if any vulnerabilities were found."""
|
|
46
|
+
return len(self.vulnerabilities) > 0
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def critical_count(self) -> int:
|
|
50
|
+
"""Count of critical severity vulnerabilities."""
|
|
51
|
+
return sum(1 for v in self.vulnerabilities if (v.severity or "").upper() == "CRITICAL")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def high_count(self) -> int:
|
|
55
|
+
"""Count of high severity vulnerabilities."""
|
|
56
|
+
return sum(1 for v in self.vulnerabilities if (v.severity or "").upper() == "HIGH")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CVESentinelScanner:
|
|
60
|
+
"""Main scanner class that orchestrates the CVE detection process.
|
|
61
|
+
|
|
62
|
+
This class coordinates the dependency analysis, vulnerability matching,
|
|
63
|
+
and result reporting for CVE Sentinel.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
config: Config,
|
|
69
|
+
nvd_client: Optional[NVDClient] = None,
|
|
70
|
+
osv_client: Optional[OSVClient] = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Initialize the scanner with configuration.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
config: Configuration object with scan settings.
|
|
76
|
+
nvd_client: Optional pre-configured NVD client.
|
|
77
|
+
osv_client: Optional pre-configured OSV client.
|
|
78
|
+
"""
|
|
79
|
+
self.config = config
|
|
80
|
+
self._nvd_client = nvd_client
|
|
81
|
+
self._osv_client = osv_client
|
|
82
|
+
self._reporter: Optional[Reporter] = None
|
|
83
|
+
self._matcher: Optional[VulnerabilityMatcher] = None
|
|
84
|
+
self._initialized = False
|
|
85
|
+
|
|
86
|
+
def _initialize_components(self, target_path: Path) -> None:
|
|
87
|
+
"""Initialize scanner components.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
target_path: Path to the project being scanned.
|
|
91
|
+
"""
|
|
92
|
+
if self._initialized:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Set up cache directory
|
|
96
|
+
cache_dir = target_path / ".cve-sentinel" / "cache"
|
|
97
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
# Initialize NVD client if not provided
|
|
100
|
+
if self._nvd_client is None and self.config.nvd_api_key:
|
|
101
|
+
self._nvd_client = NVDClient(
|
|
102
|
+
api_key=self.config.nvd_api_key,
|
|
103
|
+
cache_dir=cache_dir,
|
|
104
|
+
cache_ttl_hours=self.config.cache_ttl_hours,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Initialize OSV client if not provided
|
|
108
|
+
if self._osv_client is None:
|
|
109
|
+
self._osv_client = OSVClient(
|
|
110
|
+
cache_dir=cache_dir,
|
|
111
|
+
cache_ttl_hours=self.config.cache_ttl_hours,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Initialize matcher
|
|
115
|
+
self._matcher = VulnerabilityMatcher(
|
|
116
|
+
nvd_client=self._nvd_client,
|
|
117
|
+
osv_client=self._osv_client,
|
|
118
|
+
fetch_nvd_details=self._nvd_client is not None,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Initialize reporter
|
|
122
|
+
self._reporter = create_reporter(target_path)
|
|
123
|
+
|
|
124
|
+
# Register analyzers
|
|
125
|
+
self._register_analyzers()
|
|
126
|
+
|
|
127
|
+
self._initialized = True
|
|
128
|
+
|
|
129
|
+
def _register_analyzers(self) -> None:
|
|
130
|
+
"""Register all available dependency analyzers."""
|
|
131
|
+
# Import and register analyzers
|
|
132
|
+
from cve_sentinel.analyzers.go import GoAnalyzer
|
|
133
|
+
from cve_sentinel.analyzers.maven import GradleAnalyzer, MavenAnalyzer
|
|
134
|
+
from cve_sentinel.analyzers.npm import NpmAnalyzer
|
|
135
|
+
from cve_sentinel.analyzers.php import PhpAnalyzer
|
|
136
|
+
from cve_sentinel.analyzers.python import PythonAnalyzer
|
|
137
|
+
from cve_sentinel.analyzers.ruby import RubyAnalyzer
|
|
138
|
+
from cve_sentinel.analyzers.rust import RustAnalyzer
|
|
139
|
+
|
|
140
|
+
registry = AnalyzerRegistry.get_instance()
|
|
141
|
+
registry.clear()
|
|
142
|
+
|
|
143
|
+
analysis_level = self.config.analysis_level
|
|
144
|
+
custom_patterns = self.config.custom_patterns or {}
|
|
145
|
+
|
|
146
|
+
# Map ecosystem names to their aliases for custom_patterns lookup
|
|
147
|
+
# Users can use either the common name or the ecosystem name
|
|
148
|
+
ecosystem_aliases = {
|
|
149
|
+
"npm": ["javascript", "npm"],
|
|
150
|
+
"pypi": ["python", "pypi"],
|
|
151
|
+
"go": ["go"],
|
|
152
|
+
"maven": ["java", "maven", "gradle"],
|
|
153
|
+
"rubygems": ["ruby", "rubygems"],
|
|
154
|
+
"crates.io": ["rust", "crates.io"],
|
|
155
|
+
"packagist": ["php", "packagist"],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def get_custom_patterns_for(ecosystem: str) -> Optional[dict]:
|
|
159
|
+
"""Get custom patterns for an ecosystem, checking aliases."""
|
|
160
|
+
for alias in ecosystem_aliases.get(ecosystem, []):
|
|
161
|
+
if alias in custom_patterns:
|
|
162
|
+
return custom_patterns[alias]
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
# Register analyzers with appropriate analysis level and custom patterns
|
|
166
|
+
# Note: PythonAnalyzer uses exclude_patterns instead of analysis_level
|
|
167
|
+
registry.register(
|
|
168
|
+
NpmAnalyzer(
|
|
169
|
+
analysis_level=analysis_level,
|
|
170
|
+
custom_patterns=get_custom_patterns_for("npm"),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
registry.register(PythonAnalyzer(custom_patterns=get_custom_patterns_for("pypi")))
|
|
174
|
+
registry.register(
|
|
175
|
+
GoAnalyzer(
|
|
176
|
+
analysis_level=analysis_level,
|
|
177
|
+
custom_patterns=get_custom_patterns_for("go"),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
registry.register(
|
|
181
|
+
MavenAnalyzer(
|
|
182
|
+
analysis_level=analysis_level,
|
|
183
|
+
custom_patterns=get_custom_patterns_for("maven"),
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
registry.register(
|
|
187
|
+
GradleAnalyzer(
|
|
188
|
+
analysis_level=analysis_level,
|
|
189
|
+
custom_patterns=get_custom_patterns_for("maven"),
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
registry.register(
|
|
193
|
+
RubyAnalyzer(
|
|
194
|
+
analysis_level=analysis_level,
|
|
195
|
+
custom_patterns=get_custom_patterns_for("rubygems"),
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
registry.register(
|
|
199
|
+
RustAnalyzer(
|
|
200
|
+
analysis_level=analysis_level,
|
|
201
|
+
custom_patterns=get_custom_patterns_for("crates.io"),
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
registry.register(
|
|
205
|
+
PhpAnalyzer(
|
|
206
|
+
analysis_level=analysis_level,
|
|
207
|
+
custom_patterns=get_custom_patterns_for("packagist"),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def scan(self, target_path: Path) -> ScanResult:
|
|
212
|
+
"""Perform a CVE scan on the target path.
|
|
213
|
+
|
|
214
|
+
Executes the 10-step scan flow:
|
|
215
|
+
1. Update status.json to "scanning"
|
|
216
|
+
2. Load configuration
|
|
217
|
+
3. Validate target path
|
|
218
|
+
4. Detect dependency files
|
|
219
|
+
5. Parse packages (Level 1-2)
|
|
220
|
+
6. Import statement scanning (Level 3, if enabled)
|
|
221
|
+
7. CVE matching
|
|
222
|
+
8. Aggregate results
|
|
223
|
+
9. Write results.json
|
|
224
|
+
10. Update status.json to "completed"
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
target_path: Path to the project directory to scan.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
ScanResult with scan outcome and vulnerabilities.
|
|
231
|
+
"""
|
|
232
|
+
start_time = time.time()
|
|
233
|
+
errors: List[str] = []
|
|
234
|
+
packages: List[Package] = []
|
|
235
|
+
vulnerabilities: List[VulnerabilityMatch] = []
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Step 3: Validate target path BEFORE initialization
|
|
239
|
+
if not target_path.exists():
|
|
240
|
+
raise ConfigError(f"Target path does not exist: {target_path}")
|
|
241
|
+
if not target_path.is_dir():
|
|
242
|
+
raise ConfigError(f"Target path is not a directory: {target_path}")
|
|
243
|
+
|
|
244
|
+
# Step 1: Initialize components and update status
|
|
245
|
+
self._initialize_components(target_path)
|
|
246
|
+
if self._reporter:
|
|
247
|
+
self._reporter.update_status("scanning")
|
|
248
|
+
|
|
249
|
+
logger.info(f"Starting CVE scan for: {target_path}")
|
|
250
|
+
|
|
251
|
+
# Step 4-5: Detect and parse dependency files
|
|
252
|
+
registry = AnalyzerRegistry.get_instance()
|
|
253
|
+
analyzers = registry.get_all()
|
|
254
|
+
|
|
255
|
+
if not analyzers:
|
|
256
|
+
logger.warning("No analyzers registered")
|
|
257
|
+
errors.append("No analyzers registered")
|
|
258
|
+
|
|
259
|
+
combined_result = AnalysisResult()
|
|
260
|
+
|
|
261
|
+
for analyzer in analyzers:
|
|
262
|
+
logger.debug(f"Running {analyzer.ecosystem} analyzer")
|
|
263
|
+
try:
|
|
264
|
+
result = analyzer.analyze(
|
|
265
|
+
target_path,
|
|
266
|
+
exclude_patterns=self.config.exclude,
|
|
267
|
+
)
|
|
268
|
+
combined_result = combined_result.merge(result)
|
|
269
|
+
logger.debug(
|
|
270
|
+
f"{analyzer.ecosystem}: found {result.package_count} packages "
|
|
271
|
+
f"in {len(result.scanned_files)} files"
|
|
272
|
+
)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
error_msg = f"Error in {analyzer.ecosystem} analyzer: {e}"
|
|
275
|
+
logger.warning(error_msg)
|
|
276
|
+
errors.append(error_msg)
|
|
277
|
+
|
|
278
|
+
# Deduplicate packages
|
|
279
|
+
packages = self._deduplicate_packages(combined_result.packages)
|
|
280
|
+
errors.extend(combined_result.errors)
|
|
281
|
+
|
|
282
|
+
logger.info(f"Found {len(packages)} unique packages")
|
|
283
|
+
|
|
284
|
+
# Step 6: Import statement scanning (Level 3)
|
|
285
|
+
# Note: Level 3 scanning is handled within individual analyzers
|
|
286
|
+
# when analysis_level >= 3
|
|
287
|
+
|
|
288
|
+
# Step 7: CVE matching
|
|
289
|
+
if packages and self._matcher:
|
|
290
|
+
logger.info("Matching packages against vulnerability databases...")
|
|
291
|
+
try:
|
|
292
|
+
vulnerabilities = self._matcher.match(packages)
|
|
293
|
+
logger.info(f"Found {len(vulnerabilities)} vulnerabilities")
|
|
294
|
+
except Exception as e:
|
|
295
|
+
error_msg = f"Error during vulnerability matching: {e}"
|
|
296
|
+
logger.error(error_msg)
|
|
297
|
+
errors.append(error_msg)
|
|
298
|
+
|
|
299
|
+
# Step 8-9: Aggregate and write results
|
|
300
|
+
if self._reporter:
|
|
301
|
+
self._reporter.write_results(
|
|
302
|
+
project_path=target_path,
|
|
303
|
+
packages_scanned=len(packages),
|
|
304
|
+
vulnerabilities=vulnerabilities,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Print summary to terminal
|
|
308
|
+
self._reporter.print_summary(len(packages), vulnerabilities)
|
|
309
|
+
|
|
310
|
+
# Step 10: Update status to completed
|
|
311
|
+
if self._reporter:
|
|
312
|
+
self._reporter.update_status("completed")
|
|
313
|
+
|
|
314
|
+
scan_duration = time.time() - start_time
|
|
315
|
+
logger.info(f"Scan completed in {scan_duration:.2f}s")
|
|
316
|
+
|
|
317
|
+
return ScanResult(
|
|
318
|
+
success=True,
|
|
319
|
+
packages_scanned=len(packages),
|
|
320
|
+
vulnerabilities=vulnerabilities,
|
|
321
|
+
errors=errors,
|
|
322
|
+
scan_duration=scan_duration,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
except Exception as e:
|
|
326
|
+
# Error handling
|
|
327
|
+
scan_duration = time.time() - start_time
|
|
328
|
+
error_msg = str(e)
|
|
329
|
+
logger.error(f"Scan failed: {error_msg}")
|
|
330
|
+
errors.append(error_msg)
|
|
331
|
+
|
|
332
|
+
# Update status to error
|
|
333
|
+
if self._reporter:
|
|
334
|
+
self._reporter.update_status("error", error_message=error_msg)
|
|
335
|
+
|
|
336
|
+
return ScanResult(
|
|
337
|
+
success=False,
|
|
338
|
+
packages_scanned=len(packages),
|
|
339
|
+
vulnerabilities=vulnerabilities,
|
|
340
|
+
errors=errors,
|
|
341
|
+
scan_duration=scan_duration,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def _deduplicate_packages(self, packages: List[Package]) -> List[Package]:
|
|
345
|
+
"""Remove duplicate packages, preferring direct dependencies.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
packages: List of packages possibly with duplicates.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Deduplicated list of packages.
|
|
352
|
+
"""
|
|
353
|
+
seen: dict[tuple[str, str, str], Package] = {}
|
|
354
|
+
|
|
355
|
+
for pkg in packages:
|
|
356
|
+
key = (pkg.name, pkg.version, pkg.ecosystem)
|
|
357
|
+
if key not in seen:
|
|
358
|
+
seen[key] = pkg
|
|
359
|
+
elif pkg.is_direct and not seen[key].is_direct:
|
|
360
|
+
# Prefer direct dependency
|
|
361
|
+
seen[key] = pkg
|
|
362
|
+
|
|
363
|
+
return list(seen.values())
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def setup_logging(verbose: bool = False) -> None:
|
|
367
|
+
"""Set up logging configuration.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
verbose: Whether to enable debug logging.
|
|
371
|
+
"""
|
|
372
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
373
|
+
|
|
374
|
+
# Configure root logger
|
|
375
|
+
logging.basicConfig(
|
|
376
|
+
level=level,
|
|
377
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
378
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Suppress noisy loggers
|
|
382
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
383
|
+
logging.getLogger("requests").setLevel(logging.WARNING)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def create_argument_parser() -> argparse.ArgumentParser:
|
|
387
|
+
"""Create the CLI argument parser.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Configured ArgumentParser.
|
|
391
|
+
"""
|
|
392
|
+
parser = argparse.ArgumentParser(
|
|
393
|
+
prog="cve-sentinel",
|
|
394
|
+
description="Scan project dependencies for known CVE vulnerabilities",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
parser.add_argument(
|
|
398
|
+
"--path",
|
|
399
|
+
"-p",
|
|
400
|
+
type=Path,
|
|
401
|
+
default=Path("."),
|
|
402
|
+
help="Path to the project directory to scan (default: current directory)",
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
parser.add_argument(
|
|
406
|
+
"--config",
|
|
407
|
+
"-c",
|
|
408
|
+
type=Path,
|
|
409
|
+
default=None,
|
|
410
|
+
help="Path to configuration file (default: .cve-sentinel.yaml in project)",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
parser.add_argument(
|
|
414
|
+
"--verbose",
|
|
415
|
+
"-v",
|
|
416
|
+
action="store_true",
|
|
417
|
+
help="Enable verbose (debug) output",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
parser.add_argument(
|
|
421
|
+
"--version",
|
|
422
|
+
"-V",
|
|
423
|
+
action="version",
|
|
424
|
+
version=f"%(prog)s {__version__}",
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
parser.add_argument(
|
|
428
|
+
"--fail-on",
|
|
429
|
+
choices=["CRITICAL", "HIGH", "MEDIUM", "LOW"],
|
|
430
|
+
default="HIGH",
|
|
431
|
+
help="Exit with error code if vulnerabilities at or above this severity are found (default: HIGH)",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
parser.add_argument(
|
|
435
|
+
"--no-color",
|
|
436
|
+
action="store_true",
|
|
437
|
+
help="Disable colored output",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
parser.add_argument(
|
|
441
|
+
"--json",
|
|
442
|
+
action="store_true",
|
|
443
|
+
help="Output results in JSON format to stdout",
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
return parser
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
450
|
+
"""Main entry point for CVE Sentinel CLI.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
args: Command line arguments (defaults to sys.argv[1:]).
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Exit code (0: success, 1: vulnerabilities found, 2: error).
|
|
457
|
+
"""
|
|
458
|
+
parser = create_argument_parser()
|
|
459
|
+
parsed_args = parser.parse_args(args)
|
|
460
|
+
|
|
461
|
+
# Set up logging
|
|
462
|
+
setup_logging(verbose=parsed_args.verbose)
|
|
463
|
+
|
|
464
|
+
# Resolve target path
|
|
465
|
+
target_path = parsed_args.path.resolve()
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
# Load configuration
|
|
469
|
+
config = load_config(
|
|
470
|
+
base_path=target_path,
|
|
471
|
+
validate=True,
|
|
472
|
+
require_api_key=False, # Allow scanning without NVD API key
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Create scanner
|
|
476
|
+
scanner = CVESentinelScanner(config)
|
|
477
|
+
|
|
478
|
+
# Run scan
|
|
479
|
+
result = scanner.scan(target_path)
|
|
480
|
+
|
|
481
|
+
if not result.success:
|
|
482
|
+
logger.error("Scan failed with errors")
|
|
483
|
+
for error in result.errors:
|
|
484
|
+
logger.error(f" - {error}")
|
|
485
|
+
return 2
|
|
486
|
+
|
|
487
|
+
# Determine exit code based on vulnerabilities
|
|
488
|
+
if result.has_vulnerabilities:
|
|
489
|
+
# Check severity threshold
|
|
490
|
+
severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
|
|
491
|
+
threshold_index = severity_order.index(parsed_args.fail_on)
|
|
492
|
+
|
|
493
|
+
for vuln in result.vulnerabilities:
|
|
494
|
+
severity = (vuln.severity or "UNKNOWN").upper()
|
|
495
|
+
if severity in severity_order:
|
|
496
|
+
if severity_order.index(severity) <= threshold_index:
|
|
497
|
+
return 1
|
|
498
|
+
|
|
499
|
+
return 0
|
|
500
|
+
|
|
501
|
+
except ConfigError as e:
|
|
502
|
+
logger.error(f"Configuration error: {e}")
|
|
503
|
+
return 2
|
|
504
|
+
except KeyboardInterrupt:
|
|
505
|
+
logger.info("Scan cancelled by user")
|
|
506
|
+
return 2
|
|
507
|
+
except Exception as e:
|
|
508
|
+
logger.exception(f"Unexpected error: {e}")
|
|
509
|
+
return 2
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
if __name__ == "__main__":
|
|
513
|
+
sys.exit(main())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Source code scanners for CVE Sentinel."""
|
|
2
|
+
|
|
3
|
+
from cve_sentinel.scanners.import_scanner import (
|
|
4
|
+
ImportReference,
|
|
5
|
+
ImportScanner,
|
|
6
|
+
get_scanner_for_ecosystem,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ImportReference",
|
|
11
|
+
"ImportScanner",
|
|
12
|
+
"get_scanner_for_ecosystem",
|
|
13
|
+
]
|