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/config.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Configuration management for CVE Sentinel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConfigError(Exception):
|
|
14
|
+
"""Configuration error."""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigValidationError(ConfigError):
|
|
20
|
+
"""Configuration validation error."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Config:
|
|
27
|
+
"""CVE Sentinel configuration.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
target_path: Directory to scan for dependencies.
|
|
31
|
+
exclude: List of path patterns to exclude from scanning.
|
|
32
|
+
analysis_level: Depth of analysis (1-3).
|
|
33
|
+
1: Direct dependencies from manifest files
|
|
34
|
+
2: Transitive dependencies from lock files
|
|
35
|
+
3: Import statement scanning in source code
|
|
36
|
+
auto_scan_on_startup: Whether to automatically scan on startup.
|
|
37
|
+
cache_ttl_hours: Cache time-to-live in hours.
|
|
38
|
+
nvd_api_key: NVD API key for vulnerability data.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
target_path: Path = field(default_factory=lambda: Path("."))
|
|
42
|
+
exclude: list[str] = field(
|
|
43
|
+
default_factory=lambda: ["node_modules/", "vendor/", ".git/", "__pycache__/", "venv/"]
|
|
44
|
+
)
|
|
45
|
+
analysis_level: int = 2
|
|
46
|
+
auto_scan_on_startup: bool = True
|
|
47
|
+
cache_ttl_hours: int = 24
|
|
48
|
+
nvd_api_key: Optional[str] = None
|
|
49
|
+
custom_patterns: Optional[dict[str, dict[str, list[str]]]] = None
|
|
50
|
+
|
|
51
|
+
def __post_init__(self) -> None:
|
|
52
|
+
"""Convert target_path to Path if string."""
|
|
53
|
+
if isinstance(self.target_path, str):
|
|
54
|
+
self.target_path = Path(self.target_path)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _find_config_file(base_path: Path) -> Optional[Path]:
|
|
58
|
+
"""Find configuration file in the given directory.
|
|
59
|
+
|
|
60
|
+
Searches for .cve-sentinel.yaml first, then .cve-sentinel.yml.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
base_path: Directory to search in.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Path to config file if found, None otherwise.
|
|
67
|
+
"""
|
|
68
|
+
yaml_path = base_path / ".cve-sentinel.yaml"
|
|
69
|
+
if yaml_path.exists():
|
|
70
|
+
return yaml_path
|
|
71
|
+
|
|
72
|
+
yml_path = base_path / ".cve-sentinel.yml"
|
|
73
|
+
if yml_path.exists():
|
|
74
|
+
return yml_path
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _load_yaml_config(config_path: Path) -> dict[str, Any]:
|
|
80
|
+
"""Load configuration from YAML file.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config_path: Path to the YAML config file.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dictionary with configuration values.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ConfigError: If file cannot be read or parsed.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
with open(config_path, encoding="utf-8") as f:
|
|
93
|
+
data = yaml.safe_load(f)
|
|
94
|
+
return data if data is not None else {}
|
|
95
|
+
except yaml.YAMLError as e:
|
|
96
|
+
raise ConfigError(f"Failed to parse YAML config file: {e}") from e
|
|
97
|
+
except OSError as e:
|
|
98
|
+
raise ConfigError(f"Failed to read config file: {e}") from e
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _load_env_vars() -> dict[str, Any]:
|
|
102
|
+
"""Load configuration from environment variables.
|
|
103
|
+
|
|
104
|
+
Supported environment variables:
|
|
105
|
+
- CVE_SENTINEL_NVD_API_KEY: NVD API key
|
|
106
|
+
- CVE_SENTINEL_CONFIG_PATH: Custom config file path
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dictionary with configuration values from environment.
|
|
110
|
+
"""
|
|
111
|
+
env_config: dict[str, Any] = {}
|
|
112
|
+
|
|
113
|
+
nvd_api_key = os.environ.get("CVE_SENTINEL_NVD_API_KEY")
|
|
114
|
+
if nvd_api_key:
|
|
115
|
+
env_config["nvd_api_key"] = nvd_api_key
|
|
116
|
+
|
|
117
|
+
return env_config
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _get_config_path_from_env() -> Optional[Path]:
|
|
121
|
+
"""Get custom config file path from environment variable.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Path to config file if CVE_SENTINEL_CONFIG_PATH is set, None otherwise.
|
|
125
|
+
"""
|
|
126
|
+
config_path_str = os.environ.get("CVE_SENTINEL_CONFIG_PATH")
|
|
127
|
+
if config_path_str:
|
|
128
|
+
return Path(config_path_str)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _validate_custom_patterns(
|
|
133
|
+
custom_patterns: Optional[dict[str, dict[str, list[str]]]],
|
|
134
|
+
) -> list[str]:
|
|
135
|
+
"""Validate custom_patterns configuration.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
custom_patterns: The custom_patterns config value to validate.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of validation error messages (empty if valid).
|
|
142
|
+
"""
|
|
143
|
+
errors: list[str] = []
|
|
144
|
+
|
|
145
|
+
if custom_patterns is None:
|
|
146
|
+
return errors
|
|
147
|
+
|
|
148
|
+
if not isinstance(custom_patterns, dict):
|
|
149
|
+
errors.append("custom_patterns must be a dictionary")
|
|
150
|
+
return errors
|
|
151
|
+
|
|
152
|
+
# Valid ecosystem names (including common aliases)
|
|
153
|
+
valid_ecosystems = {
|
|
154
|
+
"javascript",
|
|
155
|
+
"npm",
|
|
156
|
+
"python",
|
|
157
|
+
"pypi",
|
|
158
|
+
"go",
|
|
159
|
+
"java",
|
|
160
|
+
"maven",
|
|
161
|
+
"gradle",
|
|
162
|
+
"ruby",
|
|
163
|
+
"rubygems",
|
|
164
|
+
"rust",
|
|
165
|
+
"crates.io",
|
|
166
|
+
"php",
|
|
167
|
+
"packagist",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
valid_pattern_types = {"manifests", "locks"}
|
|
171
|
+
|
|
172
|
+
for ecosystem, patterns_dict in custom_patterns.items():
|
|
173
|
+
if ecosystem not in valid_ecosystems:
|
|
174
|
+
errors.append(
|
|
175
|
+
f"custom_patterns: unknown ecosystem '{ecosystem}'. "
|
|
176
|
+
f"Valid ecosystems: {', '.join(sorted(valid_ecosystems))}"
|
|
177
|
+
)
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if not isinstance(patterns_dict, dict):
|
|
181
|
+
errors.append(
|
|
182
|
+
f"custom_patterns.{ecosystem}: must be a dictionary "
|
|
183
|
+
"with 'manifests' and/or 'locks' keys"
|
|
184
|
+
)
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
for pattern_type, patterns in patterns_dict.items():
|
|
188
|
+
if pattern_type not in valid_pattern_types:
|
|
189
|
+
errors.append(
|
|
190
|
+
f"custom_patterns.{ecosystem}: unknown pattern type '{pattern_type}'. "
|
|
191
|
+
"Valid types: manifests, locks"
|
|
192
|
+
)
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
if not isinstance(patterns, list):
|
|
196
|
+
errors.append(
|
|
197
|
+
f"custom_patterns.{ecosystem}.{pattern_type}: must be a list of strings"
|
|
198
|
+
)
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
for i, pattern in enumerate(patterns):
|
|
202
|
+
if not isinstance(pattern, str):
|
|
203
|
+
errors.append(
|
|
204
|
+
f"custom_patterns.{ecosystem}.{pattern_type}[{i}]: must be a string"
|
|
205
|
+
)
|
|
206
|
+
elif not pattern:
|
|
207
|
+
errors.append(
|
|
208
|
+
f"custom_patterns.{ecosystem}.{pattern_type}[{i}]: pattern cannot be empty"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return errors
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _validate_config(config: Config) -> None:
|
|
215
|
+
"""Validate configuration values.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
config: Configuration to validate.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
ConfigValidationError: If validation fails.
|
|
222
|
+
"""
|
|
223
|
+
errors: list[str] = []
|
|
224
|
+
|
|
225
|
+
# Validate analysis_level (1-3)
|
|
226
|
+
if not 1 <= config.analysis_level <= 3:
|
|
227
|
+
errors.append(f"analysis_level must be between 1 and 3, got {config.analysis_level}")
|
|
228
|
+
|
|
229
|
+
# Validate cache_ttl_hours (positive integer)
|
|
230
|
+
if config.cache_ttl_hours <= 0:
|
|
231
|
+
errors.append(f"cache_ttl_hours must be positive, got {config.cache_ttl_hours}")
|
|
232
|
+
|
|
233
|
+
# Validate target_path exists
|
|
234
|
+
if not config.target_path.exists():
|
|
235
|
+
errors.append(f"target_path does not exist: {config.target_path}")
|
|
236
|
+
|
|
237
|
+
# NVD API key is required for full functionality
|
|
238
|
+
if not config.nvd_api_key:
|
|
239
|
+
errors.append(
|
|
240
|
+
"nvd_api_key is required. Set CVE_SENTINEL_NVD_API_KEY environment variable "
|
|
241
|
+
"or add nvd_api_key to .cve-sentinel.yaml"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Validate custom_patterns
|
|
245
|
+
errors.extend(_validate_custom_patterns(config.custom_patterns))
|
|
246
|
+
|
|
247
|
+
if errors:
|
|
248
|
+
raise ConfigValidationError("\n".join(errors))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def load_config(
|
|
252
|
+
base_path: Optional[Path] = None,
|
|
253
|
+
validate: bool = True,
|
|
254
|
+
require_api_key: bool = True,
|
|
255
|
+
cli_overrides: Optional[dict[str, Any]] = None,
|
|
256
|
+
) -> Config:
|
|
257
|
+
"""Load configuration from file, environment variables, and CLI arguments.
|
|
258
|
+
|
|
259
|
+
Configuration is loaded in the following order (later values override earlier):
|
|
260
|
+
1. Default values
|
|
261
|
+
2. YAML config file (.cve-sentinel.yaml or .cve-sentinel.yml)
|
|
262
|
+
3. Environment variables
|
|
263
|
+
4. CLI arguments (highest priority)
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
base_path: Base directory to search for config file.
|
|
267
|
+
Defaults to current working directory.
|
|
268
|
+
validate: Whether to validate the configuration.
|
|
269
|
+
require_api_key: Whether to require NVD API key in validation.
|
|
270
|
+
cli_overrides: Dictionary of CLI argument overrides (highest priority).
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Loaded and optionally validated Config object.
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
ConfigError: If config file cannot be read or parsed.
|
|
277
|
+
ConfigValidationError: If validation is enabled and fails.
|
|
278
|
+
"""
|
|
279
|
+
if base_path is None:
|
|
280
|
+
base_path = Path.cwd()
|
|
281
|
+
|
|
282
|
+
# Start with defaults
|
|
283
|
+
config_data: dict[str, Any] = {}
|
|
284
|
+
|
|
285
|
+
# Check for custom config path from environment
|
|
286
|
+
env_config_path = _get_config_path_from_env()
|
|
287
|
+
if env_config_path:
|
|
288
|
+
if env_config_path.exists():
|
|
289
|
+
config_data.update(_load_yaml_config(env_config_path))
|
|
290
|
+
else:
|
|
291
|
+
raise ConfigError(
|
|
292
|
+
f"Config file specified by CVE_SENTINEL_CONFIG_PATH does not exist: "
|
|
293
|
+
f"{env_config_path}"
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
# Find config file in base_path
|
|
297
|
+
config_path = _find_config_file(base_path)
|
|
298
|
+
if config_path:
|
|
299
|
+
config_data.update(_load_yaml_config(config_path))
|
|
300
|
+
|
|
301
|
+
# Override with environment variables
|
|
302
|
+
config_data.update(_load_env_vars())
|
|
303
|
+
|
|
304
|
+
# Override with CLI arguments (highest priority)
|
|
305
|
+
if cli_overrides:
|
|
306
|
+
config_data.update(cli_overrides)
|
|
307
|
+
|
|
308
|
+
# Convert target_path to absolute path relative to base_path
|
|
309
|
+
if "target_path" in config_data:
|
|
310
|
+
target = Path(config_data["target_path"])
|
|
311
|
+
if not target.is_absolute():
|
|
312
|
+
config_data["target_path"] = base_path / target
|
|
313
|
+
else:
|
|
314
|
+
config_data["target_path"] = base_path
|
|
315
|
+
|
|
316
|
+
# Create Config object
|
|
317
|
+
config = Config(**config_data)
|
|
318
|
+
|
|
319
|
+
# Validate if requested
|
|
320
|
+
if validate:
|
|
321
|
+
if not require_api_key and not config.nvd_api_key:
|
|
322
|
+
# Skip API key validation but validate other fields
|
|
323
|
+
errors: list[str] = []
|
|
324
|
+
if not 1 <= config.analysis_level <= 3:
|
|
325
|
+
errors.append(
|
|
326
|
+
f"analysis_level must be between 1 and 3, got {config.analysis_level}"
|
|
327
|
+
)
|
|
328
|
+
if config.cache_ttl_hours <= 0:
|
|
329
|
+
errors.append(f"cache_ttl_hours must be positive, got {config.cache_ttl_hours}")
|
|
330
|
+
if not config.target_path.exists():
|
|
331
|
+
errors.append(f"target_path does not exist: {config.target_path}")
|
|
332
|
+
errors.extend(_validate_custom_patterns(config.custom_patterns))
|
|
333
|
+
if errors:
|
|
334
|
+
raise ConfigValidationError("\n".join(errors))
|
|
335
|
+
else:
|
|
336
|
+
_validate_config(config)
|
|
337
|
+
|
|
338
|
+
return config
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def get_default_config() -> Config:
|
|
342
|
+
"""Get default configuration without loading from file.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Config object with default values.
|
|
346
|
+
"""
|
|
347
|
+
return Config()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""CVE data fetchers for NVD and OSV APIs."""
|
|
2
|
+
|
|
3
|
+
from cve_sentinel.fetchers.nvd import CVEData, NVDAPIError, NVDClient, NVDRateLimitError
|
|
4
|
+
from cve_sentinel.fetchers.osv import (
|
|
5
|
+
MergedVulnerability,
|
|
6
|
+
OSVAPIError,
|
|
7
|
+
OSVClient,
|
|
8
|
+
OSVVulnerability,
|
|
9
|
+
merge_nvd_osv_data,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CVEData",
|
|
14
|
+
"MergedVulnerability",
|
|
15
|
+
"NVDAPIError",
|
|
16
|
+
"NVDClient",
|
|
17
|
+
"NVDRateLimitError",
|
|
18
|
+
"OSVAPIError",
|
|
19
|
+
"OSVClient",
|
|
20
|
+
"OSVVulnerability",
|
|
21
|
+
"merge_nvd_osv_data",
|
|
22
|
+
]
|