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/cli.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""CVE Sentinel CLI with subcommand support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from cve_sentinel.config import ConfigError, load_config
|
|
15
|
+
from cve_sentinel.scanner import CVESentinelScanner, __version__, setup_logging
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Default configuration template
|
|
20
|
+
CONFIG_TEMPLATE = """\
|
|
21
|
+
# CVE Sentinel Configuration
|
|
22
|
+
# See: https://github.com/xxx/cve-sentinel#configuration
|
|
23
|
+
|
|
24
|
+
# Target directory to scan (relative to this config file)
|
|
25
|
+
target_path: "."
|
|
26
|
+
|
|
27
|
+
# Paths to exclude from scanning (glob patterns)
|
|
28
|
+
exclude:
|
|
29
|
+
- "node_modules/"
|
|
30
|
+
- "vendor/"
|
|
31
|
+
- ".git/"
|
|
32
|
+
- "__pycache__/"
|
|
33
|
+
- "venv/"
|
|
34
|
+
- ".venv/"
|
|
35
|
+
- "dist/"
|
|
36
|
+
- "build/"
|
|
37
|
+
|
|
38
|
+
# Analysis level:
|
|
39
|
+
# 1: Direct dependencies from manifest files only
|
|
40
|
+
# 2: Include transitive dependencies from lock files
|
|
41
|
+
# 3: Include import statement scanning in source code
|
|
42
|
+
analysis_level: 2
|
|
43
|
+
|
|
44
|
+
# Automatically scan when Claude Code session starts
|
|
45
|
+
auto_scan_on_startup: true
|
|
46
|
+
|
|
47
|
+
# Cache time-to-live in hours (CVE data cache)
|
|
48
|
+
cache_ttl_hours: 24
|
|
49
|
+
|
|
50
|
+
# NVD API key (recommended for better rate limits)
|
|
51
|
+
# Get your key at: https://nvd.nist.gov/developers/request-an-api-key
|
|
52
|
+
# Best practice: Set via environment variable CVE_SENTINEL_NVD_API_KEY
|
|
53
|
+
# nvd_api_key: "your-api-key-here"
|
|
54
|
+
|
|
55
|
+
# Custom file patterns for dependency detection (optional)
|
|
56
|
+
# Use this to scan non-standard dependency files
|
|
57
|
+
# Valid ecosystems: javascript, python, go, java, ruby, rust, php
|
|
58
|
+
# custom_patterns:
|
|
59
|
+
# python:
|
|
60
|
+
# manifests:
|
|
61
|
+
# - "deps/*.txt"
|
|
62
|
+
# - "custom-requirements.txt"
|
|
63
|
+
# locks:
|
|
64
|
+
# - "custom.lock"
|
|
65
|
+
# javascript:
|
|
66
|
+
# manifests:
|
|
67
|
+
# - "dependencies.json"
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# CLAUDE.md addition template
|
|
71
|
+
CLAUDE_MD_ADDITION = """\
|
|
72
|
+
|
|
73
|
+
## CVE Sentinel Integration
|
|
74
|
+
|
|
75
|
+
This project uses CVE Sentinel for automatic vulnerability detection.
|
|
76
|
+
|
|
77
|
+
### Automatic Scanning
|
|
78
|
+
CVE Sentinel automatically scans dependencies when a Claude Code session starts.
|
|
79
|
+
Check `.cve-sentinel/status.json` for scan status and `.cve-sentinel/results.json` for results.
|
|
80
|
+
|
|
81
|
+
### Manual Commands
|
|
82
|
+
```bash
|
|
83
|
+
# Scan current directory (no config file needed)
|
|
84
|
+
cve-sentinel scan
|
|
85
|
+
|
|
86
|
+
# Scan specific path
|
|
87
|
+
cve-sentinel scan /path/to/project
|
|
88
|
+
|
|
89
|
+
# With options
|
|
90
|
+
cve-sentinel scan --level 2 --exclude "test/*" --verbose
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Configuration (Optional)
|
|
94
|
+
For persistent settings, create `.cve-sentinel.yaml`. CLI options override config file settings.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def cmd_scan(args: argparse.Namespace) -> int:
|
|
99
|
+
"""Execute the scan command."""
|
|
100
|
+
setup_logging(verbose=args.verbose)
|
|
101
|
+
|
|
102
|
+
target_path = args.path.resolve()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Build CLI overrides
|
|
106
|
+
cli_overrides: dict = {}
|
|
107
|
+
if args.level is not None:
|
|
108
|
+
cli_overrides["analysis_level"] = args.level
|
|
109
|
+
if args.exclude:
|
|
110
|
+
cli_overrides["exclude"] = args.exclude
|
|
111
|
+
|
|
112
|
+
config = load_config(
|
|
113
|
+
base_path=target_path,
|
|
114
|
+
validate=True,
|
|
115
|
+
require_api_key=False,
|
|
116
|
+
cli_overrides=cli_overrides,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
scanner = CVESentinelScanner(config)
|
|
120
|
+
result = scanner.scan(target_path)
|
|
121
|
+
|
|
122
|
+
if not result.success:
|
|
123
|
+
logger.error("Scan failed with errors")
|
|
124
|
+
for error in result.errors:
|
|
125
|
+
logger.error(f" - {error}")
|
|
126
|
+
return 2
|
|
127
|
+
|
|
128
|
+
# Determine exit code based on vulnerabilities
|
|
129
|
+
if result.has_vulnerabilities:
|
|
130
|
+
severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW"]
|
|
131
|
+
threshold_index = severity_order.index(args.fail_on)
|
|
132
|
+
|
|
133
|
+
for vuln in result.vulnerabilities:
|
|
134
|
+
severity = (vuln.severity or "UNKNOWN").upper()
|
|
135
|
+
if severity in severity_order:
|
|
136
|
+
if severity_order.index(severity) <= threshold_index:
|
|
137
|
+
return 1
|
|
138
|
+
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
except ConfigError as e:
|
|
142
|
+
logger.error(f"Configuration error: {e}")
|
|
143
|
+
return 2
|
|
144
|
+
except KeyboardInterrupt:
|
|
145
|
+
logger.info("Scan cancelled by user")
|
|
146
|
+
return 2
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.exception(f"Unexpected error: {e}")
|
|
149
|
+
return 2
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
153
|
+
"""Execute the init command."""
|
|
154
|
+
setup_logging(verbose=args.verbose)
|
|
155
|
+
|
|
156
|
+
target_path = args.path.resolve()
|
|
157
|
+
|
|
158
|
+
print(f"Initializing CVE Sentinel in: {target_path}")
|
|
159
|
+
|
|
160
|
+
# Create .cve-sentinel.yaml
|
|
161
|
+
config_file = target_path / ".cve-sentinel.yaml"
|
|
162
|
+
if config_file.exists() and not args.force:
|
|
163
|
+
print(f"Configuration file already exists: {config_file}")
|
|
164
|
+
print("Use --force to overwrite.")
|
|
165
|
+
else:
|
|
166
|
+
config_file.write_text(CONFIG_TEMPLATE)
|
|
167
|
+
print(f"Created: {config_file}")
|
|
168
|
+
|
|
169
|
+
# Create .cve-sentinel directory
|
|
170
|
+
sentinel_dir = target_path / ".cve-sentinel"
|
|
171
|
+
sentinel_dir.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
print(f"Created: {sentinel_dir}/")
|
|
173
|
+
|
|
174
|
+
# Add to .gitignore if it exists
|
|
175
|
+
gitignore = target_path / ".gitignore"
|
|
176
|
+
gitignore_entries = [".cve-sentinel/"]
|
|
177
|
+
|
|
178
|
+
if gitignore.exists():
|
|
179
|
+
content = gitignore.read_text()
|
|
180
|
+
lines_to_add = [entry for entry in gitignore_entries if entry not in content]
|
|
181
|
+
if lines_to_add:
|
|
182
|
+
with open(gitignore, "a") as f:
|
|
183
|
+
f.write("\n# CVE Sentinel\n")
|
|
184
|
+
for entry in lines_to_add:
|
|
185
|
+
f.write(f"{entry}\n")
|
|
186
|
+
print(f"Updated: {gitignore}")
|
|
187
|
+
else:
|
|
188
|
+
with open(gitignore, "w") as f:
|
|
189
|
+
f.write("# CVE Sentinel\n")
|
|
190
|
+
for entry in gitignore_entries:
|
|
191
|
+
f.write(f"{entry}\n")
|
|
192
|
+
print(f"Created: {gitignore}")
|
|
193
|
+
|
|
194
|
+
# Show CLAUDE.md addition suggestion
|
|
195
|
+
print("\n" + "=" * 50)
|
|
196
|
+
print("Consider adding the following to your CLAUDE.md:")
|
|
197
|
+
print("=" * 50)
|
|
198
|
+
print(CLAUDE_MD_ADDITION)
|
|
199
|
+
print("=" * 50)
|
|
200
|
+
|
|
201
|
+
print("\nCVE Sentinel initialized successfully!")
|
|
202
|
+
print("\nNext steps:")
|
|
203
|
+
print(" 1. Run: cve-sentinel scan")
|
|
204
|
+
print(" 2. (Optional) Customize .cve-sentinel.yaml for persistent settings")
|
|
205
|
+
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def cmd_uninstall(args: argparse.Namespace) -> int:
|
|
210
|
+
"""Execute the uninstall command."""
|
|
211
|
+
setup_logging(verbose=args.verbose)
|
|
212
|
+
|
|
213
|
+
print("Uninstalling CVE Sentinel...")
|
|
214
|
+
|
|
215
|
+
# Confirm uninstall
|
|
216
|
+
if not args.yes:
|
|
217
|
+
response = input("Are you sure you want to uninstall CVE Sentinel? [y/N] ")
|
|
218
|
+
if response.lower() not in ("y", "yes"):
|
|
219
|
+
print("Uninstall cancelled.")
|
|
220
|
+
return 0
|
|
221
|
+
|
|
222
|
+
errors = []
|
|
223
|
+
|
|
224
|
+
# Remove Claude Code hook settings
|
|
225
|
+
settings_file = Path.home() / ".claude" / "settings.json"
|
|
226
|
+
if settings_file.exists():
|
|
227
|
+
try:
|
|
228
|
+
with open(settings_file) as f:
|
|
229
|
+
settings = json.load(f)
|
|
230
|
+
|
|
231
|
+
# Remove CVE Sentinel hook
|
|
232
|
+
if "hooks" in settings and "sessionStart" in settings["hooks"]:
|
|
233
|
+
settings["hooks"]["sessionStart"] = [
|
|
234
|
+
h
|
|
235
|
+
for h in settings["hooks"]["sessionStart"]
|
|
236
|
+
if not (isinstance(h, dict) and h.get("name") == "cve-sentinel")
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
with open(settings_file, "w") as f:
|
|
240
|
+
json.dump(settings, f, indent=2)
|
|
241
|
+
print("Removed Claude Code hook settings")
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
errors.append(f"Failed to update Claude Code settings: {e}")
|
|
245
|
+
|
|
246
|
+
# Remove hook script
|
|
247
|
+
hook_script = Path.home() / ".claude" / "hooks" / "cve-sentinel-scan.sh"
|
|
248
|
+
if hook_script.exists():
|
|
249
|
+
try:
|
|
250
|
+
hook_script.unlink()
|
|
251
|
+
print(f"Removed: {hook_script}")
|
|
252
|
+
except Exception as e:
|
|
253
|
+
errors.append(f"Failed to remove hook script: {e}")
|
|
254
|
+
|
|
255
|
+
# Remove cache (optional)
|
|
256
|
+
if args.remove_cache:
|
|
257
|
+
cache_dir = Path.home() / ".cve-sentinel"
|
|
258
|
+
if cache_dir.exists():
|
|
259
|
+
try:
|
|
260
|
+
shutil.rmtree(cache_dir)
|
|
261
|
+
print(f"Removed cache: {cache_dir}")
|
|
262
|
+
except Exception as e:
|
|
263
|
+
errors.append(f"Failed to remove cache: {e}")
|
|
264
|
+
|
|
265
|
+
# Uninstall pip package
|
|
266
|
+
try:
|
|
267
|
+
result = subprocess.run(
|
|
268
|
+
[sys.executable, "-m", "pip", "uninstall", "-y", "cve-sentinel"],
|
|
269
|
+
capture_output=True,
|
|
270
|
+
text=True,
|
|
271
|
+
)
|
|
272
|
+
if result.returncode == 0:
|
|
273
|
+
print("Uninstalled CVE Sentinel package")
|
|
274
|
+
else:
|
|
275
|
+
errors.append(f"pip uninstall failed: {result.stderr}")
|
|
276
|
+
except Exception as e:
|
|
277
|
+
errors.append(f"Failed to uninstall package: {e}")
|
|
278
|
+
|
|
279
|
+
if errors:
|
|
280
|
+
print("\nCompleted with errors:")
|
|
281
|
+
for error in errors:
|
|
282
|
+
print(f" - {error}")
|
|
283
|
+
return 1
|
|
284
|
+
|
|
285
|
+
print("\nCVE Sentinel has been uninstalled.")
|
|
286
|
+
print("\nNote: Project-level .cve-sentinel.yaml and .cve-sentinel/ directories")
|
|
287
|
+
print("have not been removed. Delete them manually if desired.")
|
|
288
|
+
|
|
289
|
+
return 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def cmd_update(args: argparse.Namespace) -> int:
|
|
293
|
+
"""Execute the update command."""
|
|
294
|
+
setup_logging(verbose=args.verbose)
|
|
295
|
+
|
|
296
|
+
print("Updating CVE Sentinel...")
|
|
297
|
+
|
|
298
|
+
# Update pip package
|
|
299
|
+
try:
|
|
300
|
+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "cve-sentinel"]
|
|
301
|
+
if args.verbose:
|
|
302
|
+
result = subprocess.run(cmd, text=True)
|
|
303
|
+
else:
|
|
304
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
305
|
+
|
|
306
|
+
if result.returncode == 0:
|
|
307
|
+
print("CVE Sentinel package updated successfully")
|
|
308
|
+
else:
|
|
309
|
+
if result.stderr:
|
|
310
|
+
print(f"Update failed: {result.stderr}")
|
|
311
|
+
return 1
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
print(f"Failed to update package: {e}")
|
|
315
|
+
return 1
|
|
316
|
+
|
|
317
|
+
# Update hook script
|
|
318
|
+
hooks_dir = Path.home() / ".claude" / "hooks"
|
|
319
|
+
hook_script = hooks_dir / "cve-sentinel-scan.sh"
|
|
320
|
+
|
|
321
|
+
if hooks_dir.exists():
|
|
322
|
+
hook_content = """\
|
|
323
|
+
#!/bin/bash
|
|
324
|
+
# CVE Sentinel SessionStart Hook
|
|
325
|
+
# This script is called when a Claude Code session starts
|
|
326
|
+
|
|
327
|
+
# Get the project directory from argument or current directory
|
|
328
|
+
PROJECT_DIR="${1:-.}"
|
|
329
|
+
|
|
330
|
+
# Check if .cve-sentinel.yaml exists or if there are dependency files
|
|
331
|
+
should_scan() {
|
|
332
|
+
if [ -f "$PROJECT_DIR/.cve-sentinel.yaml" ]; then
|
|
333
|
+
return 0
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
# Check for common dependency files
|
|
337
|
+
for file in package.json requirements.txt pyproject.toml Gemfile Cargo.toml go.mod composer.json pom.xml build.gradle; do
|
|
338
|
+
if [ -f "$PROJECT_DIR/$file" ]; then
|
|
339
|
+
return 0
|
|
340
|
+
fi
|
|
341
|
+
done
|
|
342
|
+
|
|
343
|
+
return 1
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
# Run scan if applicable
|
|
347
|
+
if should_scan; then
|
|
348
|
+
# Run in background to not block Claude Code startup
|
|
349
|
+
nohup cve-sentinel scan --path "$PROJECT_DIR" > /dev/null 2>&1 &
|
|
350
|
+
fi
|
|
351
|
+
"""
|
|
352
|
+
try:
|
|
353
|
+
hook_script.write_text(hook_content)
|
|
354
|
+
hook_script.chmod(0o755)
|
|
355
|
+
print(f"Updated hook script: {hook_script}")
|
|
356
|
+
except Exception as e:
|
|
357
|
+
print(f"Warning: Failed to update hook script: {e}")
|
|
358
|
+
|
|
359
|
+
print("\nCVE Sentinel has been updated to the latest version.")
|
|
360
|
+
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
365
|
+
"""Create the main argument parser with subcommands."""
|
|
366
|
+
parser = argparse.ArgumentParser(
|
|
367
|
+
prog="cve-sentinel",
|
|
368
|
+
description="CVE auto-detection and remediation for project dependencies",
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
parser.add_argument(
|
|
372
|
+
"--version",
|
|
373
|
+
"-V",
|
|
374
|
+
action="version",
|
|
375
|
+
version=f"%(prog)s {__version__}",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
subparsers = parser.add_subparsers(
|
|
379
|
+
title="commands",
|
|
380
|
+
dest="command",
|
|
381
|
+
description="Available commands",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Scan command (default behavior)
|
|
385
|
+
scan_parser = subparsers.add_parser(
|
|
386
|
+
"scan",
|
|
387
|
+
help="Scan project for CVE vulnerabilities",
|
|
388
|
+
description="Scan project dependencies for known CVE vulnerabilities",
|
|
389
|
+
)
|
|
390
|
+
scan_parser.add_argument(
|
|
391
|
+
"path",
|
|
392
|
+
type=Path,
|
|
393
|
+
nargs="?",
|
|
394
|
+
default=Path("."),
|
|
395
|
+
help="Path to the project directory (default: current directory)",
|
|
396
|
+
)
|
|
397
|
+
scan_parser.add_argument(
|
|
398
|
+
"--level",
|
|
399
|
+
"-l",
|
|
400
|
+
type=int,
|
|
401
|
+
choices=[1, 2, 3],
|
|
402
|
+
default=None,
|
|
403
|
+
help="Analysis level: 1=manifest only, 2=include lock files, 3=include import scanning",
|
|
404
|
+
)
|
|
405
|
+
scan_parser.add_argument(
|
|
406
|
+
"--exclude",
|
|
407
|
+
"-e",
|
|
408
|
+
action="append",
|
|
409
|
+
help="Path patterns to exclude (can be specified multiple times)",
|
|
410
|
+
)
|
|
411
|
+
scan_parser.add_argument(
|
|
412
|
+
"--verbose",
|
|
413
|
+
"-v",
|
|
414
|
+
action="store_true",
|
|
415
|
+
help="Enable verbose output",
|
|
416
|
+
)
|
|
417
|
+
scan_parser.add_argument(
|
|
418
|
+
"--fail-on",
|
|
419
|
+
choices=["CRITICAL", "HIGH", "MEDIUM", "LOW"],
|
|
420
|
+
default="HIGH",
|
|
421
|
+
help="Exit with error if vulnerabilities at or above this severity (default: HIGH)",
|
|
422
|
+
)
|
|
423
|
+
scan_parser.set_defaults(func=cmd_scan)
|
|
424
|
+
|
|
425
|
+
# Init command
|
|
426
|
+
init_parser = subparsers.add_parser(
|
|
427
|
+
"init",
|
|
428
|
+
help="Initialize CVE Sentinel in a project",
|
|
429
|
+
description="Create configuration files for CVE Sentinel in the project",
|
|
430
|
+
)
|
|
431
|
+
init_parser.add_argument(
|
|
432
|
+
"--path",
|
|
433
|
+
"-p",
|
|
434
|
+
type=Path,
|
|
435
|
+
default=Path("."),
|
|
436
|
+
help="Path to the project directory (default: current directory)",
|
|
437
|
+
)
|
|
438
|
+
init_parser.add_argument(
|
|
439
|
+
"--force",
|
|
440
|
+
"-f",
|
|
441
|
+
action="store_true",
|
|
442
|
+
help="Overwrite existing configuration",
|
|
443
|
+
)
|
|
444
|
+
init_parser.add_argument(
|
|
445
|
+
"--verbose",
|
|
446
|
+
"-v",
|
|
447
|
+
action="store_true",
|
|
448
|
+
help="Enable verbose output",
|
|
449
|
+
)
|
|
450
|
+
init_parser.set_defaults(func=cmd_init)
|
|
451
|
+
|
|
452
|
+
# Uninstall command
|
|
453
|
+
uninstall_parser = subparsers.add_parser(
|
|
454
|
+
"uninstall",
|
|
455
|
+
help="Uninstall CVE Sentinel",
|
|
456
|
+
description="Remove CVE Sentinel and its configuration",
|
|
457
|
+
)
|
|
458
|
+
uninstall_parser.add_argument(
|
|
459
|
+
"--yes",
|
|
460
|
+
"-y",
|
|
461
|
+
action="store_true",
|
|
462
|
+
help="Skip confirmation prompt",
|
|
463
|
+
)
|
|
464
|
+
uninstall_parser.add_argument(
|
|
465
|
+
"--remove-cache",
|
|
466
|
+
action="store_true",
|
|
467
|
+
help="Also remove cached CVE data",
|
|
468
|
+
)
|
|
469
|
+
uninstall_parser.add_argument(
|
|
470
|
+
"--verbose",
|
|
471
|
+
"-v",
|
|
472
|
+
action="store_true",
|
|
473
|
+
help="Enable verbose output",
|
|
474
|
+
)
|
|
475
|
+
uninstall_parser.set_defaults(func=cmd_uninstall)
|
|
476
|
+
|
|
477
|
+
# Update command
|
|
478
|
+
update_parser = subparsers.add_parser(
|
|
479
|
+
"update",
|
|
480
|
+
help="Update CVE Sentinel to the latest version",
|
|
481
|
+
description="Update CVE Sentinel package and hook scripts",
|
|
482
|
+
)
|
|
483
|
+
update_parser.add_argument(
|
|
484
|
+
"--verbose",
|
|
485
|
+
"-v",
|
|
486
|
+
action="store_true",
|
|
487
|
+
help="Enable verbose output",
|
|
488
|
+
)
|
|
489
|
+
update_parser.set_defaults(func=cmd_update)
|
|
490
|
+
|
|
491
|
+
return parser
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def main(args: Optional[List[str]] = None) -> int:
|
|
495
|
+
"""Main entry point for CVE Sentinel CLI."""
|
|
496
|
+
parser = create_parser()
|
|
497
|
+
|
|
498
|
+
# Handle shorthand: if first arg is not a known command, treat as path for scan
|
|
499
|
+
known_commands = {"scan", "init", "uninstall", "update"}
|
|
500
|
+
if args and args[0] not in known_commands and not args[0].startswith("-"):
|
|
501
|
+
# First arg looks like a path, prepend "scan"
|
|
502
|
+
args = ["scan", *args]
|
|
503
|
+
|
|
504
|
+
parsed_args = parser.parse_args(args)
|
|
505
|
+
|
|
506
|
+
# Default to scan if no command specified
|
|
507
|
+
if parsed_args.command is None:
|
|
508
|
+
# No arguments at all - run scan on current directory
|
|
509
|
+
scan_args = ["scan"]
|
|
510
|
+
parsed_args = parser.parse_args(scan_args)
|
|
511
|
+
|
|
512
|
+
# Execute command
|
|
513
|
+
if hasattr(parsed_args, "func"):
|
|
514
|
+
return parsed_args.func(parsed_args)
|
|
515
|
+
|
|
516
|
+
parser.print_help()
|
|
517
|
+
return 0
|