-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkeystore.py
More file actions
328 lines (273 loc) · 9.75 KB
/
Copy pathkeystore.py
File metadata and controls
328 lines (273 loc) · 9.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
"""Personal-mode API key storage in a user-writable keys.env file.
Keys are stored as ENV_VAR=value pairs. load_into_environ() is called at startup
to inject file keys into os.environ (existing env vars take precedence).
NOTE: WIZARD_ORDER and the order used in web_ui.py's _SETTINGS_PROVIDER_ORDER must stay in sync.
CONFIG_ENTRIES applies to all interfaces (wizard, console set-key, web UI).
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from enrichment import PROVIDER_SPECS
# Broad target types first, then IP-only, then domain/URL-only.
WIZARD_ORDER = [
"virustotal",
"viewdns",
"mxtoolbox",
"dnsdb",
"urlscan",
"shodan",
"censys",
"spur",
"abuseipdb",
"greynoise",
"otx",
"dnsdumpster",
"securitytrails",
]
# Non-key configuration values shown inline with provider key rows in all UIs.
# Stored in keys.env so users can set them without editing environment variables.
# "before" means the entry appears just above the named provider's key row.
CONFIG_ENTRIES: list[dict] = [
{
"name": "dnsdb_root",
"env_var": "DNSDB_API_ROOT",
"display_name": "DNSDB API Root",
"before": "dnsdb",
"is_url": True,
"description": (
"Custom DNSDB API root URL, e.g. https://fsi-NNNN.dnsdb.info/dnsdb/v2. "
"Leave blank to use the default public endpoint (https://api.dnsdb.info/dnsdb/v2)."
),
},
]
# Lookup tables built once from CONFIG_ENTRIES
_CONFIG_BY_NAME: dict[str, dict] = {e["name"]: e for e in CONFIG_ENTRIES}
# provider → list of config entries that appear before that provider's key row
_CONFIG_BEFORE: dict[str, list[dict]] = {}
for _ce in CONFIG_ENTRIES:
_CONFIG_BEFORE.setdefault(_ce.get("before", ""), []).append(_ce)
_TARGET_LABELS: dict[tuple[str, ...], str] = {
("ip", "domain", "url"): "IP · Domain · URL",
("ip", "domain"): "IP · Domain",
("ip", "asn"): "IP · ASN",
("ip",): "IP",
("domain", "url"): "Domain · URL",
("asn",): "ASN",
}
def _target_label(provider: str) -> str:
spec = PROVIDER_SPECS.get(provider)
if not spec:
return ""
return _TARGET_LABELS.get(tuple(spec.target_types), " · ".join(spec.target_types))
def _keys_dir() -> Path:
# STEALTHOPS_KEYS_DIR lets WSL2 point at the Windows key store so both
# the Windows and Linux binaries share the same keys.env file.
# Set automatically by the installer; can also be set manually.
override = os.environ.get("STEALTHOPS_KEYS_DIR")
if override:
return Path(override)
if sys.platform == "win32":
base = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
return Path(base) / "StealthOps"
return Path.home() / ".config" / "stealthops"
def _keys_file() -> Path:
return _keys_dir() / "keys.env"
def _read_file() -> dict[str, str]:
try:
result: dict[str, str] = {}
for line in _keys_file().read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip()
if key:
result[key] = value
return result
except Exception:
return {}
def _write_file(data: dict[str, str]) -> None:
try:
_keys_dir().mkdir(parents=True, exist_ok=True)
lines = [f"{k}={v}" for k, v in sorted(data.items()) if v]
_keys_file().write_text("\n".join(lines) + ("\n" if lines else ""))
except Exception:
pass
def _primary_env_var(provider: str) -> str | None:
spec = PROVIDER_SPECS.get(provider)
return spec.env_vars[0] if spec and spec.env_vars else None
def load_into_environ() -> None:
"""Inject file keys into os.environ. Existing env vars are not overwritten."""
for env_var, value in _read_file().items():
if env_var not in os.environ and value:
os.environ[env_var] = value
def sync_into_environ() -> None:
"""Re-read the keys file and overwrite os.environ with current values.
Unlike load_into_environ, this always applies file values so that keys
changed externally (web UI in another process, another terminal) are
picked up immediately. Called on each console REPL iteration.
"""
for env_var, value in _read_file().items():
if value:
os.environ[env_var] = value
else:
os.environ.pop(env_var, None)
def set_key(provider: str, key: str) -> bool:
"""Save or clear a key. Returns False if provider is unknown or has no env var."""
env_var = _primary_env_var(provider)
if not env_var:
return False
data = _read_file()
if key:
data[env_var] = key
os.environ[env_var] = key
else:
data.pop(env_var, None)
os.environ.pop(env_var, None)
_write_file(data)
return True
def delete_key(provider: str) -> bool:
return set_key(provider, "")
def set_config(name: str, value: str) -> bool:
"""Save or clear a CONFIG_ENTRIES value by its name. Returns False if unknown."""
entry = _CONFIG_BY_NAME.get(name)
if not entry:
return False
env_var = entry["env_var"]
data = _read_file()
if value:
data[env_var] = value
os.environ[env_var] = value
else:
data.pop(env_var, None)
os.environ.pop(env_var, None)
_write_file(data)
return True
def delete_config(name: str) -> bool:
return set_config(name, "")
def get_config(name: str) -> dict:
"""Return info dict for a single CONFIG_ENTRIES value (includes value and source)."""
entry = _CONFIG_BY_NAME.get(name)
if not entry:
return {}
env_var = entry["env_var"]
file_data = _read_file()
file_value = file_data.get(env_var, "")
env_value = os.environ.get(env_var, "")
if file_value:
source: str | None = "file"
value = file_value
elif env_value:
source = "env"
value = env_value
else:
source = None
value = ""
return {**entry, "value": value, "source": source}
def mask(value: str) -> str:
if not value:
return ""
if len(value) <= 4:
return "••••"
return "••••••••" + value[-4:]
def get_all() -> dict[str, dict]:
"""Return key info for all key-bearing providers in WIZARD_ORDER.
Each entry has: value, source ("env"|"file"|None), masked, display_name,
env_var, target_label.
source="env" means the key came from an environment variable not in the keys
file — it is not editable via the UI.
"""
file_data = _read_file()
result: dict[str, dict] = {}
for provider in WIZARD_ORDER:
spec = PROVIDER_SPECS.get(provider)
if not spec or not spec.env_vars:
continue
env_var = spec.env_vars[0]
file_value = file_data.get(env_var, "")
env_value = os.environ.get(env_var, "")
if file_value:
source: str | None = "file"
value = file_value
elif env_value:
source = "env"
value = env_value
else:
source = None
value = ""
result[provider] = {
"value": value,
"source": source,
"masked": mask(value),
"display_name": spec.display_name,
"env_var": env_var,
"target_label": _target_label(provider),
}
return result
def run_setup_wizard() -> None:
"""Walk through all providers interactively. Saves each key immediately;
Ctrl+C stops early but keeps whatever was already saved."""
print("")
print(" StealthOps API Key Setup")
print(" " + "─" * 38)
print(" Press Enter to keep an existing value.")
print(" Type 'done' to finish early, Ctrl+C to stop and keep saved keys.")
print("")
all_keys = get_all()
changes = 0
for provider in WIZARD_ORDER:
# Config entries that appear before this provider's key row
for cfg in _CONFIG_BEFORE.get(provider, []):
cfg_info = get_config(cfg["name"])
cfg_source = cfg_info.get("source")
cfg_current = cfg_info.get("value", "")
cfg_display = cfg["display_name"]
if cfg_source == "env":
print(f" {cfg_display:<22} {cfg_current!r} [env — skipping]")
continue
suffix = f" [{cfg_current!r}]" if cfg_current else " [not set — blank uses default]"
prompt = f" {cfg_display}{suffix}: "
try:
new_val = input(prompt).strip()
except (EOFError, KeyboardInterrupt):
print("")
return
if new_val.lower() == "done":
return
if new_val == "":
continue
set_config(cfg["name"], new_val)
changes += 1
print(" [saved]")
info = all_keys.get(provider)
if not info:
continue
source = info["source"]
current = info["value"]
display = info["display_name"]
label = info["target_label"]
if source == "env":
print(f" {display:<22} {mask(current)} [env — skipping]")
continue
suffix = f" [{mask(current)}]" if current else " [not set]"
prompt = f" {display} ({label}){suffix}: "
try:
new_val = input(prompt).strip()
except (EOFError, KeyboardInterrupt):
print("")
break
if new_val.lower() == "done":
break
if new_val == "":
continue
set_key(provider, new_val)
changes += 1
print(" [saved]")
print("")
if changes:
print(f" {changes} key(s) saved.")
else:
print(" No changes made.")
print("")