cveye 3.2.0__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.
- cveye/__init__.py +3 -0
- cveye/__main__.py +8 -0
- cveye/batch.py +204 -0
- cveye/cache.py +235 -0
- cveye/cli.py +129 -0
- cveye/colors.py +265 -0
- cveye/commands/__init__.py +1 -0
- cveye/commands/cve.py +339 -0
- cveye/commands/intel.py +323 -0
- cveye/commands/tools.py +200 -0
- cveye/config.py +139 -0
- cveye/cvss4.py +647 -0
- cveye/downloader.py +236 -0
- cveye/exploits.py +638 -0
- cveye/notify.py +119 -0
- cveye/output.py +213 -0
- cveye/parser.py +449 -0
- cveye/py.typed +0 -0
- cveye/report.py +263 -0
- cveye/sarif.py +129 -0
- cveye/sources.py +473 -0
- cveye/triage.py +162 -0
- cveye/utils.py +388 -0
- cveye-3.2.0.dist-info/METADATA +511 -0
- cveye-3.2.0.dist-info/RECORD +29 -0
- cveye-3.2.0.dist-info/WHEEL +5 -0
- cveye-3.2.0.dist-info/entry_points.txt +2 -0
- cveye-3.2.0.dist-info/licenses/LICENSE +21 -0
- cveye-3.2.0.dist-info/top_level.txt +1 -0
cveye/__init__.py
ADDED
cveye/__main__.py
ADDED
cveye/batch.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stdin batch engine: reads CVE IDs (or nuclei/httpx JSONL) from stdin,
|
|
3
|
+
warms the cache via one OSV batch request, then processes each CVE
|
|
4
|
+
concurrently with per-thread stdout capture so output blocks never
|
|
5
|
+
interleave.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
15
|
+
|
|
16
|
+
from .colors import set_silent, tag_error, tag_success
|
|
17
|
+
from .output import elapsed
|
|
18
|
+
from .utils import normalize_cve_id, dedup
|
|
19
|
+
|
|
20
|
+
_STDIN_CVE_RE = re.compile(r"CVE-\d{4}-\d{4,7}", re.I)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extract_cves_from_line(line):
|
|
24
|
+
"""
|
|
25
|
+
Extract CVE IDs from a stdin line.
|
|
26
|
+
|
|
27
|
+
Accepts plain CVE IDs ('CVE-2021-44228', '2021-44228') as well as
|
|
28
|
+
structured output from other tools - any JSON blob such as nuclei or
|
|
29
|
+
httpx result lines is scanned for CVE identifiers.
|
|
30
|
+
Returns a list of normalized IDs (may be empty).
|
|
31
|
+
"""
|
|
32
|
+
stripped = line.strip()
|
|
33
|
+
if stripped.startswith("{") or stripped.startswith("["):
|
|
34
|
+
found = _STDIN_CVE_RE.findall(stripped)
|
|
35
|
+
return dedup([normalize_cve_id(c) for c in found if normalize_cve_id(c)])
|
|
36
|
+
normalized = normalize_cve_id(stripped)
|
|
37
|
+
return [normalized] if normalized else []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_cve_ids_from_stdin():
|
|
41
|
+
"""
|
|
42
|
+
Read CVE IDs from stdin (one per line). JSON tool output (nuclei,
|
|
43
|
+
httpx, ...) is auto-detected per line and scanned for CVE IDs.
|
|
44
|
+
Returns (valid_ids, skipped_lines).
|
|
45
|
+
"""
|
|
46
|
+
if sys.stdin.isatty():
|
|
47
|
+
return [], []
|
|
48
|
+
valid, skipped = [], []
|
|
49
|
+
seen = set()
|
|
50
|
+
for line in sys.stdin:
|
|
51
|
+
line = line.rstrip("\n")
|
|
52
|
+
if not line.strip():
|
|
53
|
+
continue
|
|
54
|
+
ids = extract_cves_from_line(line)
|
|
55
|
+
if ids:
|
|
56
|
+
for cid in ids:
|
|
57
|
+
if cid not in seen:
|
|
58
|
+
seen.add(cid)
|
|
59
|
+
valid.append(cid)
|
|
60
|
+
else:
|
|
61
|
+
skipped.append(line.strip())
|
|
62
|
+
return valid, skipped
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BatchStdoutProxy:
|
|
66
|
+
"""
|
|
67
|
+
stdout proxy used during concurrent batch processing.
|
|
68
|
+
|
|
69
|
+
Each worker thread captures its own writes into a private buffer so
|
|
70
|
+
result blocks never interleave; the main thread passes through to the
|
|
71
|
+
real stdout. Installed once for the whole batch.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, real):
|
|
75
|
+
self._real = real
|
|
76
|
+
self._local = threading.local()
|
|
77
|
+
|
|
78
|
+
def write(self, s):
|
|
79
|
+
buf = getattr(self._local, "buffer", None)
|
|
80
|
+
if buf is None:
|
|
81
|
+
return self._real.write(s)
|
|
82
|
+
return buf.write(s)
|
|
83
|
+
|
|
84
|
+
def flush(self):
|
|
85
|
+
buf = getattr(self._local, "buffer", None)
|
|
86
|
+
if buf is None:
|
|
87
|
+
self._real.flush()
|
|
88
|
+
else:
|
|
89
|
+
buf.flush()
|
|
90
|
+
|
|
91
|
+
def start_capture(self):
|
|
92
|
+
self._local.buffer = io.StringIO()
|
|
93
|
+
|
|
94
|
+
def stop_capture(self):
|
|
95
|
+
buf = getattr(self._local, "buffer", None)
|
|
96
|
+
self._local.buffer = None
|
|
97
|
+
return buf.getvalue() if buf else ""
|
|
98
|
+
|
|
99
|
+
def __getattr__(self, name):
|
|
100
|
+
return getattr(self._real, name)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def handle_stdin_cves(args, func):
|
|
104
|
+
"""
|
|
105
|
+
Handle stdin input for CVE IDs ('-' as cve_id).
|
|
106
|
+
|
|
107
|
+
Supports batch processing of multiple CVEs, executed concurrently
|
|
108
|
+
(-c/-concurrency, default 5). Result blocks are kept intact; JSON batch
|
|
109
|
+
mode emits a single JSON array in input order, with failures reported
|
|
110
|
+
as {"cve_id": ..., "error": ...} objects instead of being dropped.
|
|
111
|
+
"""
|
|
112
|
+
cve_ids, skipped = read_cve_ids_from_stdin()
|
|
113
|
+
|
|
114
|
+
for line in skipped[:3]:
|
|
115
|
+
from .colors import tag_warning
|
|
116
|
+
tag_warning(f"Skipping invalid CVE ID: {line}")
|
|
117
|
+
if len(skipped) > 3:
|
|
118
|
+
from .colors import tag_warning
|
|
119
|
+
tag_warning(f"... and {len(skipped) - 3} more invalid lines skipped")
|
|
120
|
+
|
|
121
|
+
if not cve_ids:
|
|
122
|
+
tag_error("No valid CVE IDs found on stdin.")
|
|
123
|
+
return 1
|
|
124
|
+
|
|
125
|
+
concurrency = getattr(args, "concurrency", None) or 5
|
|
126
|
+
concurrency = max(1, min(int(concurrency), len(cve_ids)))
|
|
127
|
+
|
|
128
|
+
was_silent = getattr(args, "silent", False)
|
|
129
|
+
if args.json and not was_silent:
|
|
130
|
+
set_silent(True)
|
|
131
|
+
|
|
132
|
+
# Warm the cache: one OSV querybatch round-trip resolves aliases and
|
|
133
|
+
# fills the cache so the per-CVE phase is mostly cache hits.
|
|
134
|
+
try:
|
|
135
|
+
from .sources import fetch_cve_details_batch
|
|
136
|
+
fetch_cve_details_batch(cve_ids, max_workers=concurrency)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
real_stdout = sys.stdout
|
|
141
|
+
proxy = BatchStdoutProxy(real_stdout)
|
|
142
|
+
sys.stdout = proxy
|
|
143
|
+
|
|
144
|
+
exit_code = 0
|
|
145
|
+
print_lock = threading.Lock()
|
|
146
|
+
|
|
147
|
+
def _work(cid):
|
|
148
|
+
new_args = argparse.Namespace(**vars(args))
|
|
149
|
+
new_args.cve_id = cid
|
|
150
|
+
proxy.start_capture()
|
|
151
|
+
try:
|
|
152
|
+
code = func(new_args)
|
|
153
|
+
out = proxy.stop_capture()
|
|
154
|
+
return cid, code, out, None
|
|
155
|
+
except Exception as e: # keep one bad CVE from killing the batch
|
|
156
|
+
proxy.stop_capture()
|
|
157
|
+
return cid, 1, "", e
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if concurrency == 1 or len(cve_ids) == 1:
|
|
161
|
+
outcomes = [_work(cid) for cid in cve_ids]
|
|
162
|
+
else:
|
|
163
|
+
outcomes = []
|
|
164
|
+
with ThreadPoolExecutor(max_workers=concurrency) as ex:
|
|
165
|
+
futures = [ex.submit(_work, cid) for cid in cve_ids]
|
|
166
|
+
for fut in as_completed(futures):
|
|
167
|
+
outcomes.append(fut.result())
|
|
168
|
+
|
|
169
|
+
if args.json:
|
|
170
|
+
by_id = {}
|
|
171
|
+
for cid, code, out, err in outcomes:
|
|
172
|
+
if err is not None or code != 0:
|
|
173
|
+
by_id[cid] = {"cve_id": cid, "error": "lookup failed"}
|
|
174
|
+
exit_code = code or 1
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
by_id[cid] = json.loads(out.strip())
|
|
178
|
+
except json.JSONDecodeError:
|
|
179
|
+
by_id[cid] = {"cve_id": cid,
|
|
180
|
+
"error": "invalid JSON output"}
|
|
181
|
+
exit_code = 1
|
|
182
|
+
# Preserve input order in the emitted array.
|
|
183
|
+
batch = [by_id.get(cid) for cid in cve_ids]
|
|
184
|
+
with print_lock:
|
|
185
|
+
print(json.dumps(batch, indent=2))
|
|
186
|
+
else:
|
|
187
|
+
for cid, code, out, err in outcomes:
|
|
188
|
+
with print_lock:
|
|
189
|
+
if out:
|
|
190
|
+
print(out, end="" if out.endswith("\n") else "\n")
|
|
191
|
+
if err is not None:
|
|
192
|
+
with print_lock:
|
|
193
|
+
tag_error(f"Error processing {cid}: {err}")
|
|
194
|
+
exit_code = 1
|
|
195
|
+
elif code != 0:
|
|
196
|
+
exit_code = code
|
|
197
|
+
finally:
|
|
198
|
+
sys.stdout = real_stdout
|
|
199
|
+
if args.json and not was_silent:
|
|
200
|
+
set_silent(False)
|
|
201
|
+
|
|
202
|
+
tag_success(f"Processed {len(cve_ids)} CVE(s) in {elapsed()} "
|
|
203
|
+
f"(concurrency {concurrency})")
|
|
204
|
+
return exit_code
|
cveye/cache.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Persistent TTL cache for CVEye.
|
|
3
|
+
|
|
4
|
+
Stores API responses (CVE details, EPSS scores, KEV index, ExploitDB CSV)
|
|
5
|
+
under the user cache directory so repeated runs are instant and
|
|
6
|
+
rate-limit friendly. Thread-safe; disabled at runtime via -no-cache or
|
|
7
|
+
CVEYE_NO_CACHE=1.
|
|
8
|
+
|
|
9
|
+
Backends:
|
|
10
|
+
- json (default): two JSON files (cache.json + big-cache.json)
|
|
11
|
+
- sqlite: a single SQLite database; better for very large batches.
|
|
12
|
+
Selected with CVEYE_CACHE_BACKEND=sqlite (stdlib sqlite3 only).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import tempfile
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
DEFAULT_TTL = 3600
|
|
22
|
+
|
|
23
|
+
_enabled = True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def set_enabled(enabled):
|
|
27
|
+
"""Enable/disable caching globally (disabled caches are no-ops)."""
|
|
28
|
+
global _enabled
|
|
29
|
+
_enabled = bool(enabled)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_enabled():
|
|
33
|
+
return _enabled
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def default_cache_dir():
|
|
37
|
+
"""Platform-appropriate cache directory for CVEye."""
|
|
38
|
+
if os.name == "nt":
|
|
39
|
+
base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
|
40
|
+
return os.path.join(base, "CVEye", "cache")
|
|
41
|
+
base = os.environ.get("XDG_CACHE_HOME") or os.path.join(
|
|
42
|
+
os.path.expanduser("~"), ".cache")
|
|
43
|
+
return os.path.join(base, "cveye")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def user_cache_path(filename="cache.json"):
|
|
47
|
+
return os.path.join(default_cache_dir(), filename)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TTLCache:
|
|
51
|
+
"""A JSON-file-backed key/value store with per-entry TTLs."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, path):
|
|
54
|
+
self.path = path
|
|
55
|
+
self.ttl = DEFAULT_TTL
|
|
56
|
+
self._lock = threading.Lock()
|
|
57
|
+
self._data = {}
|
|
58
|
+
self._load()
|
|
59
|
+
|
|
60
|
+
# -- persistence ------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def _load(self):
|
|
63
|
+
try:
|
|
64
|
+
with open(self.path, "r", encoding="utf-8") as f:
|
|
65
|
+
raw = json.load(f)
|
|
66
|
+
if isinstance(raw, dict):
|
|
67
|
+
self._data = raw.get("entries", {})
|
|
68
|
+
except (OSError, ValueError):
|
|
69
|
+
self._data = {}
|
|
70
|
+
|
|
71
|
+
def save(self):
|
|
72
|
+
"""Atomically persist non-expired entries."""
|
|
73
|
+
with self._lock:
|
|
74
|
+
entries = {k: v for k, v in self._data.items()
|
|
75
|
+
if v.get("e", 0) > time.time()}
|
|
76
|
+
self._data = entries
|
|
77
|
+
try:
|
|
78
|
+
os.makedirs(os.path.dirname(self.path), exist_ok=True)
|
|
79
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(self.path),
|
|
80
|
+
prefix=".cveye-cache-")
|
|
81
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
82
|
+
json.dump({"version": 1, "entries": entries}, f)
|
|
83
|
+
os.replace(tmp, self.path)
|
|
84
|
+
except OSError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# -- api ----------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def get(self, key):
|
|
90
|
+
"""Return cached value or None if missing/expired/disabled."""
|
|
91
|
+
if not _enabled:
|
|
92
|
+
return None
|
|
93
|
+
with self._lock:
|
|
94
|
+
entry = self._data.get(key)
|
|
95
|
+
if not entry:
|
|
96
|
+
return None
|
|
97
|
+
if entry.get("e", 0) <= time.time():
|
|
98
|
+
del self._data[key]
|
|
99
|
+
return None
|
|
100
|
+
return entry.get("v")
|
|
101
|
+
|
|
102
|
+
def set(self, key, value, ttl=None):
|
|
103
|
+
"""Store value with TTL (seconds) and persist."""
|
|
104
|
+
if not _enabled:
|
|
105
|
+
return
|
|
106
|
+
with self._lock:
|
|
107
|
+
self._data[key] = {
|
|
108
|
+
"v": value,
|
|
109
|
+
"e": time.time() + (ttl if ttl is not None else self.ttl),
|
|
110
|
+
}
|
|
111
|
+
self.save()
|
|
112
|
+
|
|
113
|
+
def clear(self):
|
|
114
|
+
with self._lock:
|
|
115
|
+
self._data = {}
|
|
116
|
+
try:
|
|
117
|
+
os.remove(self.path)
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SQLiteCache:
|
|
123
|
+
"""
|
|
124
|
+
SQLite-backed key/value store with per-entry TTLs.
|
|
125
|
+
|
|
126
|
+
Same interface as TTLCache. Values are stored as JSON text; reads hit
|
|
127
|
+
the database directly so huge batches don't hold everything in memory.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, path):
|
|
131
|
+
import sqlite3
|
|
132
|
+
self.path = path
|
|
133
|
+
self.ttl = DEFAULT_TTL
|
|
134
|
+
self._lock = threading.Lock()
|
|
135
|
+
try:
|
|
136
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
137
|
+
self._conn = sqlite3.connect(path, check_same_thread=False)
|
|
138
|
+
self._conn.execute(
|
|
139
|
+
"CREATE TABLE IF NOT EXISTS cache ("
|
|
140
|
+
" key TEXT PRIMARY KEY, value TEXT NOT NULL,"
|
|
141
|
+
" expires REAL NOT NULL)")
|
|
142
|
+
self._conn.commit()
|
|
143
|
+
except OSError:
|
|
144
|
+
# Fall back to a disabled in-memory instance on failure.
|
|
145
|
+
self._conn = sqlite3.connect(":memory:")
|
|
146
|
+
self._conn.execute(
|
|
147
|
+
"CREATE TABLE IF NOT EXISTS cache ("
|
|
148
|
+
" key TEXT PRIMARY KEY, value TEXT NOT NULL,"
|
|
149
|
+
" expires REAL NOT NULL)")
|
|
150
|
+
|
|
151
|
+
def get(self, key):
|
|
152
|
+
if not _enabled:
|
|
153
|
+
return None
|
|
154
|
+
row = self._conn.execute(
|
|
155
|
+
"SELECT value, expires FROM cache WHERE key = ?",
|
|
156
|
+
(key,)).fetchone()
|
|
157
|
+
if row is None:
|
|
158
|
+
return None
|
|
159
|
+
if row[1] <= time.time():
|
|
160
|
+
with self._lock:
|
|
161
|
+
self._conn.execute("DELETE FROM cache WHERE key = ?", (key,))
|
|
162
|
+
self._conn.commit()
|
|
163
|
+
return None
|
|
164
|
+
try:
|
|
165
|
+
return json.loads(row[0])
|
|
166
|
+
except ValueError:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def set(self, key, value, ttl=None):
|
|
170
|
+
if not _enabled:
|
|
171
|
+
return
|
|
172
|
+
expires = time.time() + (ttl if ttl is not None else self.ttl)
|
|
173
|
+
with self._lock:
|
|
174
|
+
self._conn.execute(
|
|
175
|
+
"INSERT OR REPLACE INTO cache (key, value, expires)"
|
|
176
|
+
" VALUES (?, ?, ?)", (key, json.dumps(value), expires))
|
|
177
|
+
self._conn.commit()
|
|
178
|
+
|
|
179
|
+
def clear(self):
|
|
180
|
+
with self._lock:
|
|
181
|
+
self._conn.execute("DELETE FROM cache")
|
|
182
|
+
self._conn.commit()
|
|
183
|
+
try:
|
|
184
|
+
os.remove(self.path)
|
|
185
|
+
except OSError:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
def save(self):
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _make_cache(filename):
|
|
193
|
+
backend = os.environ.get("CVEYE_CACHE_BACKEND", "json").lower()
|
|
194
|
+
if backend == "sqlite":
|
|
195
|
+
return SQLiteCache(user_cache_path("cache.db"))
|
|
196
|
+
return TTLCache(user_cache_path(filename))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# Shared process-wide cache instances. The 'big' variant holds bulky
|
|
200
|
+
# payloads (KEV index, ExploitDB CSV index) so frequent small writes never
|
|
201
|
+
# rewrite megabyte-sized files.
|
|
202
|
+
_instance = _make_cache("cache.json")
|
|
203
|
+
if isinstance(_instance, SQLiteCache):
|
|
204
|
+
_instance_big = _instance # single DB serves both sizes
|
|
205
|
+
else:
|
|
206
|
+
_instance_big = TTLCache(user_cache_path("big-cache.json"))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# Module-level accessors so callers can simply `from . import cache`
|
|
210
|
+
# and use cache.get(...) / cache.set(...).
|
|
211
|
+
|
|
212
|
+
def get(key):
|
|
213
|
+
return _instance.get(key)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def set(key, value, ttl=None):
|
|
217
|
+
_instance.set(key, value, ttl)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_big(key):
|
|
221
|
+
return _instance_big.get(key)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def set_big(key, value, ttl=None):
|
|
225
|
+
_instance_big.set(key, value, ttl)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def save():
|
|
229
|
+
_instance.save()
|
|
230
|
+
_instance_big.save()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def clear():
|
|
234
|
+
_instance.clear()
|
|
235
|
+
_instance_big.clear()
|
cveye/cli.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CVEye CLI entry point - Check latest CVEs and exploit availability
|
|
4
|
+
without API keys.
|
|
5
|
+
|
|
6
|
+
The implementation is split across focused modules:
|
|
7
|
+
parser.py argument parsing and help formatting
|
|
8
|
+
output.py presentation helpers (banner, CVE printers)
|
|
9
|
+
batch.py stdin batch engine
|
|
10
|
+
commands/ one module per command group
|
|
11
|
+
|
|
12
|
+
This module wires them together: global-flag application (config file,
|
|
13
|
+
env vars), stream redirection, and main().
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
if sys.version_info < (3, 7):
|
|
19
|
+
sys.stderr.write("CVEye requires Python 3.7 or newer.\n")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
from . import cache
|
|
23
|
+
from .config import VERSION, load_user_config
|
|
24
|
+
from .colors import (
|
|
25
|
+
set_color,
|
|
26
|
+
set_silent,
|
|
27
|
+
refresh_streams,
|
|
28
|
+
tag_error,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Re-exported for backward compatibility (tests and external callers).
|
|
32
|
+
from .parser import build_parser, CVeyeParser, CVeyeHelpFormatter # noqa: F401
|
|
33
|
+
from .output import print_startup as _print_startup # noqa: F401
|
|
34
|
+
from .batch import ( # noqa: F401
|
|
35
|
+
handle_stdin_cves as handle_stdin_cve,
|
|
36
|
+
read_cve_ids_from_stdin as _read_cve_ids_from_stdin,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def apply_global_args(args):
|
|
41
|
+
"""Apply global flags, filling unset values from the user config file.
|
|
42
|
+
|
|
43
|
+
Precedence: CLI flag > config.json > environment/built-in default.
|
|
44
|
+
"""
|
|
45
|
+
import os as _os
|
|
46
|
+
|
|
47
|
+
from . import config as _config
|
|
48
|
+
|
|
49
|
+
cfg = load_user_config()
|
|
50
|
+
|
|
51
|
+
if getattr(args, "silent", None) is None:
|
|
52
|
+
args.silent = bool(cfg.get("silent", False))
|
|
53
|
+
if getattr(args, "no_color", None) is None:
|
|
54
|
+
args.no_color = bool(cfg.get("no_color", False))
|
|
55
|
+
if getattr(args, "no_cache", None) is None:
|
|
56
|
+
args.no_cache = bool(cfg.get("no_cache", False)) or \
|
|
57
|
+
bool(_os.environ.get("CVEYE_NO_CACHE"))
|
|
58
|
+
if getattr(args, "concurrency", None) is None:
|
|
59
|
+
try:
|
|
60
|
+
args.concurrency = max(1, int(cfg.get("concurrency", 5)))
|
|
61
|
+
except (TypeError, ValueError):
|
|
62
|
+
args.concurrency = 5
|
|
63
|
+
if getattr(args, "download_dir", None) is None:
|
|
64
|
+
args.download_dir = cfg.get("download_dir", "exploits")
|
|
65
|
+
|
|
66
|
+
# GitHub/NVD tokens: env var wins over config file.
|
|
67
|
+
if not _os.environ.get("GITHUB_TOKEN") and cfg.get("github_token"):
|
|
68
|
+
_config.GITHUB_TOKEN = cfg["github_token"]
|
|
69
|
+
if not _os.environ.get("NVD_API_KEY") and cfg.get("nvd_api_key"):
|
|
70
|
+
_config.NVD_API_KEY = cfg["nvd_api_key"]
|
|
71
|
+
|
|
72
|
+
cache.set_enabled(not args.no_cache)
|
|
73
|
+
|
|
74
|
+
if getattr(args, "silent", False):
|
|
75
|
+
set_silent(True)
|
|
76
|
+
if getattr(args, "no_color", False):
|
|
77
|
+
set_color(False)
|
|
78
|
+
if getattr(args, "output", None):
|
|
79
|
+
try:
|
|
80
|
+
sys.stdout = open(args.output, "w", encoding="utf-8")
|
|
81
|
+
except OSError as e:
|
|
82
|
+
tag_error(f"Cannot open output file '{args.output}': {e}")
|
|
83
|
+
raise SystemExit(2)
|
|
84
|
+
# Re-evaluate color support now that stdout points at a file.
|
|
85
|
+
refresh_streams()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv=None):
|
|
89
|
+
"""Main entry point."""
|
|
90
|
+
from .output import reset_timer, print_startup
|
|
91
|
+
|
|
92
|
+
reset_timer()
|
|
93
|
+
parser = build_parser()
|
|
94
|
+
args = parser.parse_args(argv)
|
|
95
|
+
|
|
96
|
+
if not args.command:
|
|
97
|
+
# Show the banner on bare invocation too.
|
|
98
|
+
if not getattr(args, "silent", False):
|
|
99
|
+
print_startup()
|
|
100
|
+
parser.print_help()
|
|
101
|
+
return 1
|
|
102
|
+
|
|
103
|
+
apply_global_args(args)
|
|
104
|
+
|
|
105
|
+
# Show banner and warnings unless silent or JSON mode.
|
|
106
|
+
if (not getattr(args, "silent", False)
|
|
107
|
+
and not getattr(args, "json", False)):
|
|
108
|
+
print_startup()
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
if (args.command in ("check", "scan", "search", "affected")
|
|
112
|
+
and getattr(args, "cve_id", "") == "-"):
|
|
113
|
+
from .batch import handle_stdin_cves
|
|
114
|
+
code = handle_stdin_cves(args, args.func)
|
|
115
|
+
else:
|
|
116
|
+
code = args.func(args)
|
|
117
|
+
finally:
|
|
118
|
+
if getattr(args, "output", None):
|
|
119
|
+
try:
|
|
120
|
+
sys.stdout.close()
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
sys.stdout = sys.__stdout__
|
|
124
|
+
|
|
125
|
+
return code
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
sys.exit(main())
|