printerxpl-forge 6.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.
- nse/README.md +204 -0
- nse/__init__.py +6 -0
- nse/install_nse.py +412 -0
- nse/lib/printerxpl.lua +238 -0
- nse/scripts/cups-info.nse +74 -0
- nse/scripts/cups-queue-info.nse +43 -0
- nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
- nse/scripts/http-device-mac.nse +107 -0
- nse/scripts/http-hp-ilo-info.nse +121 -0
- nse/scripts/http-info-xerox-enum.nse +101 -0
- nse/scripts/http-vuln-cve2022-1026.nse +158 -0
- nse/scripts/lexmark-config.nse +89 -0
- nse/scripts/pjl-ready-message.nse +106 -0
- nse/scripts/printer-banner.nse +217 -0
- nse/scripts/printer-cups-rce.nse +189 -0
- nse/scripts/printer-cve-detect.nse +279 -0
- nse/scripts/printer-discover.nse +205 -0
- nse/scripts/printer-firmware-exposed.nse +219 -0
- nse/scripts/printer-hp-pjl.nse +192 -0
- nse/scripts/printer-http-ews.nse +293 -0
- nse/scripts/printer-ipp-info.nse +235 -0
- nse/scripts/printer-lexmark-ipp.nse +203 -0
- nse/scripts/printer-passback.nse +204 -0
- nse/scripts/printer-pjl-info.nse +146 -0
- nse/scripts/printer-printnightmare.nse +211 -0
- nse/scripts/printer-snmp-info.nse +176 -0
- nse/scripts/printer-vuln-check.nse +256 -0
- nse/scripts/snmp-device-mac.nse +93 -0
- nse/scripts/snmp-info.nse +146 -0
- nse/scripts/snmp-sysdescr.nse +70 -0
- printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
- printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
- printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
- printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
- printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
- printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
- src/assets/fonts/gunplay.pfa +1671 -0
- src/assets/fonts/kshandwrt.pfa +315 -0
- src/assets/fonts/laksoner.pfa +2402 -0
- src/assets/fonts/paintcans.pfa +9699 -0
- src/assets/fonts/stencilod.pfa +4076 -0
- src/assets/fonts/takecover.pfa +26138 -0
- src/assets/fonts/topsecret.pfa +6652 -0
- src/assets/fonts/whoa.pfa +773 -0
- src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
- src/assets/mibs/Printer-MIB +4389 -0
- src/assets/mibs/README.md +9 -0
- src/assets/mibs/SNMPv2-MIB +854 -0
- src/assets/overlays/hacker.eps +596 -0
- src/assets/overlays/smiley.eps +214 -0
- src/assets/overlays/smiley2.eps +240 -0
- src/core/attack_orchestrator.py +1025 -0
- src/core/capabilities.py +323 -0
- src/core/destructive_audit.py +430 -0
- src/core/discovery.py +488 -0
- src/core/osdetect.py +74 -0
- src/core/poly_runner.py +579 -0
- src/core/printer.py +1426 -0
- src/main.py +2134 -0
- src/modules/install_printer.py +318 -0
- src/modules/login_bruteforce.py +852 -0
- src/modules/pcl.py +506 -0
- src/modules/pjl.py +3575 -0
- src/modules/print_job.py +1290 -0
- src/modules/ps.py +1102 -0
- src/payloads/__init__.py +98 -0
- src/payloads/assets/overlays/notice.eps +9 -0
- src/protocols/__init__.py +19 -0
- src/protocols/firmware.py +738 -0
- src/protocols/ipp.py +216 -0
- src/protocols/ipp_attacks.py +609 -0
- src/protocols/lpd.py +141 -0
- src/protocols/network_map.py +1004 -0
- src/protocols/raw.py +173 -0
- src/protocols/smb.py +359 -0
- src/protocols/ssrf_pivot.py +427 -0
- src/protocols/storage.py +587 -0
- src/ui/__init__.py +6 -0
- src/ui/interactive.py +742 -0
- src/ui/spinner.py +112 -0
- src/ui/tables.py +132 -0
- src/utils/banner_grabber.py +852 -0
- src/utils/codebook.py +456 -0
- src/utils/config.py +522 -0
- src/utils/cve_loader.py +158 -0
- src/utils/default_creds.py +134 -0
- src/utils/discovery_online.py +1327 -0
- src/utils/exploit_manager.py +805 -0
- src/utils/fuzzer.py +220 -0
- src/utils/helper.py +732 -0
- src/utils/local_printers.py +307 -0
- src/utils/ml_engine.py +491 -0
- src/utils/operators.py +474 -0
- src/utils/ports.py +234 -0
- src/utils/vuln_scanner.py +823 -0
- src/utils/wordlist_loader.py +412 -0
- src/version.py +36 -0
src/utils/config.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PrinterXPL-Forge — Configuration Loader
|
|
5
|
+
======================================
|
|
6
|
+
Loads config.json (JSON) from the project root, a path set via the
|
|
7
|
+
--config CLI flag, or the PrinterXPL-Forge_CONFIG environment variable.
|
|
8
|
+
Also accepts the legacy config.yaml format (requires pyyaml).
|
|
9
|
+
|
|
10
|
+
Supports multiple API keys per provider (first non-empty key is used).
|
|
11
|
+
Validates which features are available based on configured credentials
|
|
12
|
+
and warns the user when a feature is called without required keys.
|
|
13
|
+
|
|
14
|
+
Resolution order (highest priority first):
|
|
15
|
+
1. CLI flag: --config /path/to/config.json
|
|
16
|
+
2. Environment variable: PrinterXPL-Forge_CONFIG
|
|
17
|
+
3. config.json in project root (next to src/)
|
|
18
|
+
4. config.yaml in project root (legacy fallback)
|
|
19
|
+
5. Built-in defaults (no credentials — limited features)
|
|
20
|
+
|
|
21
|
+
Override individual keys via environment variables:
|
|
22
|
+
SHODAN_API_KEY, CENSYS_API_ID, CENSYS_API_SECRET,
|
|
23
|
+
NVD_API_KEY, VIRUSTOTAL_API_KEY, GREYNOISE_API_KEY,
|
|
24
|
+
ABUSE_IPDB_API_KEY, OPENAI_API_KEY
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Author : Andre Henrique (@mrhenrike)
|
|
28
|
+
# GitHub : https://github.com/mrhenrike
|
|
29
|
+
# LinkedIn : https://linkedin.com/in/mrhenrike
|
|
30
|
+
# X/Twitter : https://x.com/mrhenrike
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
39
|
+
|
|
40
|
+
_log = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# ── Project root detection ────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
_HERE = Path(__file__).resolve().parent # src/utils/
|
|
45
|
+
_PROJECT_ROOT = _HERE.parent.parent # project root
|
|
46
|
+
|
|
47
|
+
# ── Feature → credential mapping ─────────────────────────────────────────────
|
|
48
|
+
# Maps feature names to human-readable labels and the config key(s) needed.
|
|
49
|
+
|
|
50
|
+
FEATURE_REQUIREMENTS: Dict[str, Dict] = {
|
|
51
|
+
'shodan_search': {
|
|
52
|
+
'label': 'Shodan search / --discover-online',
|
|
53
|
+
'section': 'shodan',
|
|
54
|
+
'keys': ['api_key'],
|
|
55
|
+
'url': 'https://account.shodan.io/',
|
|
56
|
+
},
|
|
57
|
+
'censys_search': {
|
|
58
|
+
'label': 'Censys search / --discover-online',
|
|
59
|
+
'section': 'censys',
|
|
60
|
+
'keys': ['api_id', 'api_secret'],
|
|
61
|
+
'url': 'https://search.censys.io/account/api',
|
|
62
|
+
},
|
|
63
|
+
'fofa_search': {
|
|
64
|
+
'label': 'FOFA search / --discover-online --dork-engine fofa',
|
|
65
|
+
'section': 'fofa',
|
|
66
|
+
'keys': ['email', 'api_key'],
|
|
67
|
+
'url': 'https://en.fofa.info/api',
|
|
68
|
+
},
|
|
69
|
+
'zoomeye_search': {
|
|
70
|
+
'label': 'ZoomEye search / --discover-online --dork-engine zoomeye',
|
|
71
|
+
'section': 'zoomeye',
|
|
72
|
+
'keys': ['api_key'],
|
|
73
|
+
'url': 'https://www.zoomeye.org/profile',
|
|
74
|
+
},
|
|
75
|
+
'netlas_search': {
|
|
76
|
+
'label': 'Netlas search / --discover-online --dork-engine netlas',
|
|
77
|
+
'section': 'netlas',
|
|
78
|
+
'keys': ['api_key'],
|
|
79
|
+
'url': 'https://app.netlas.io/profile/',
|
|
80
|
+
},
|
|
81
|
+
'anthropic': {
|
|
82
|
+
'label': 'Anthropic Claude LLM — AI analysis, report generation',
|
|
83
|
+
'section': 'anthropic',
|
|
84
|
+
'keys': ['api_key'],
|
|
85
|
+
'url': 'https://console.anthropic.com/settings/keys',
|
|
86
|
+
'optional': True,
|
|
87
|
+
},
|
|
88
|
+
'gemini': {
|
|
89
|
+
'label': 'Google Gemini LLM — AI analysis, report generation',
|
|
90
|
+
'section': 'gemini',
|
|
91
|
+
'keys': ['api_key'],
|
|
92
|
+
'url': 'https://aistudio.google.com/app/apikey',
|
|
93
|
+
'optional': True,
|
|
94
|
+
},
|
|
95
|
+
'nvd_lookup': {
|
|
96
|
+
'label': 'NVD CVE lookup (higher rate limit)',
|
|
97
|
+
'section': 'nvd',
|
|
98
|
+
'keys': ['api_key'],
|
|
99
|
+
'url': 'https://nvd.nist.gov/developers/request-an-api-key',
|
|
100
|
+
'optional': True, # NVD works without key, just rate-limited
|
|
101
|
+
},
|
|
102
|
+
'openai': {
|
|
103
|
+
'label': 'OpenAI — AI-assisted analysis, report generation',
|
|
104
|
+
'section': 'openai',
|
|
105
|
+
'keys': ['api_key'],
|
|
106
|
+
'url': 'https://platform.openai.com/api-keys',
|
|
107
|
+
'optional': True,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# ── Defaults ──────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
_DEFAULTS: Dict = {
|
|
114
|
+
'shodan': [{'label': 'primary', 'api_key': ''}],
|
|
115
|
+
'censys': [{'label': 'primary', 'api_id': '', 'api_secret': ''}],
|
|
116
|
+
'fofa': [{'label': 'primary', 'email': '', 'api_key': ''}],
|
|
117
|
+
'zoomeye': [{'label': 'primary', 'api_key': ''}],
|
|
118
|
+
'netlas': [{'label': 'primary', 'api_key': ''}],
|
|
119
|
+
'nvd': [{'label': 'primary', 'api_key': ''}],
|
|
120
|
+
'openai': [{'label': 'primary', 'api_key': '', 'model': 'gpt-4o'}],
|
|
121
|
+
'anthropic': [{'label': 'primary', 'api_key': '', 'model': 'claude-opus-4-5'}],
|
|
122
|
+
'gemini': [{'label': 'primary', 'api_key': '', 'model': 'gemini-2.0-flash'}],
|
|
123
|
+
'network': {'timeout': 5, 'snmp_community': 'public', 'snmp_timeout': 2, 'max_retries': 1},
|
|
124
|
+
'ml': {'enabled': False, 'model_dir': '.ml_models', 'min_confidence': 0.60},
|
|
125
|
+
'discovery': {'max_results_per_query': 50, 'delay_between_queries': 1.5},
|
|
126
|
+
'output': {'color': True, 'verbose': False, 'log_dir': '.log'},
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# ── Module state ──────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
_config: Dict | None = None
|
|
132
|
+
_config_path: Path | None = None
|
|
133
|
+
_load_errors: List[str] = []
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ── Loader ────────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
def _find_config_file(explicit_path: str | None = None) -> Optional[Path]:
|
|
139
|
+
"""Return the first valid config file path, or None."""
|
|
140
|
+
candidates = []
|
|
141
|
+
|
|
142
|
+
if explicit_path:
|
|
143
|
+
candidates.append(Path(explicit_path))
|
|
144
|
+
env_path = os.environ.get('PrinterXPL-Forge_CONFIG')
|
|
145
|
+
if env_path:
|
|
146
|
+
candidates.append(Path(env_path))
|
|
147
|
+
|
|
148
|
+
candidates.extend([
|
|
149
|
+
_PROJECT_ROOT / 'config.json',
|
|
150
|
+
_PROJECT_ROOT / 'config.yaml',
|
|
151
|
+
Path.cwd() / 'config.json',
|
|
152
|
+
Path.cwd() / 'config.yaml',
|
|
153
|
+
])
|
|
154
|
+
|
|
155
|
+
for p in candidates:
|
|
156
|
+
if p.exists():
|
|
157
|
+
return p
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _load_json(path: Path) -> Dict:
|
|
162
|
+
with open(path, encoding='utf-8') as fh:
|
|
163
|
+
raw = json.load(fh)
|
|
164
|
+
# Strip the _comment key (documentation only)
|
|
165
|
+
raw.pop('_comment', None)
|
|
166
|
+
return raw
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _load_yaml(path: Path) -> Dict:
|
|
170
|
+
try:
|
|
171
|
+
import yaml
|
|
172
|
+
except ImportError:
|
|
173
|
+
raise ImportError("pyyaml not installed — cannot load .yaml config. "
|
|
174
|
+
"Run: pip install pyyaml OR use config.json instead.")
|
|
175
|
+
with open(path, encoding='utf-8') as fh:
|
|
176
|
+
return yaml.safe_load(fh) or {}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _deep_merge(base: Dict, override: Dict) -> Dict:
|
|
180
|
+
"""Recursively merge override into base."""
|
|
181
|
+
result = dict(base)
|
|
182
|
+
for k, v in override.items():
|
|
183
|
+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
|
184
|
+
result[k] = _deep_merge(result[k], v)
|
|
185
|
+
else:
|
|
186
|
+
result[k] = v
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _normalise_provider(section: Any) -> List[Dict]:
|
|
191
|
+
"""
|
|
192
|
+
Normalise a provider entry to a list of credential dicts.
|
|
193
|
+
|
|
194
|
+
Accepts both the new list format and the legacy single-dict format:
|
|
195
|
+
new: [{"label": "primary", "api_key": "..."}]
|
|
196
|
+
legacy: {"api_key": "..."}
|
|
197
|
+
"""
|
|
198
|
+
if isinstance(section, list):
|
|
199
|
+
return section
|
|
200
|
+
if isinstance(section, dict):
|
|
201
|
+
return [section]
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def load_config(path: str | None = None) -> Dict:
|
|
206
|
+
"""
|
|
207
|
+
Load and return the merged configuration dict.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
path: Explicit path to config file (overrides all auto-detection).
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
The merged configuration dict.
|
|
214
|
+
"""
|
|
215
|
+
global _config, _config_path, _load_errors
|
|
216
|
+
_load_errors = []
|
|
217
|
+
|
|
218
|
+
cfg_path = _find_config_file(path)
|
|
219
|
+
merged = dict(_DEFAULTS)
|
|
220
|
+
|
|
221
|
+
if cfg_path:
|
|
222
|
+
try:
|
|
223
|
+
if cfg_path.suffix == '.json':
|
|
224
|
+
file_cfg = _load_json(cfg_path)
|
|
225
|
+
else:
|
|
226
|
+
file_cfg = _load_yaml(cfg_path)
|
|
227
|
+
merged = _deep_merge(merged, file_cfg)
|
|
228
|
+
_config_path = cfg_path
|
|
229
|
+
_log.debug("Loaded config from %s", cfg_path)
|
|
230
|
+
except Exception as exc:
|
|
231
|
+
msg = f"Cannot read config file {cfg_path}: {exc}"
|
|
232
|
+
_load_errors.append(msg)
|
|
233
|
+
_log.warning(msg)
|
|
234
|
+
else:
|
|
235
|
+
_log.debug("No config file found — using defaults (no credentials)")
|
|
236
|
+
|
|
237
|
+
# Override with environment variables
|
|
238
|
+
_apply_env_overrides(merged)
|
|
239
|
+
|
|
240
|
+
_config = merged
|
|
241
|
+
return merged
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _apply_env_overrides(cfg: Dict) -> None:
|
|
245
|
+
"""Apply environment variable overrides to the config dict."""
|
|
246
|
+
env_map = {
|
|
247
|
+
'SHODAN_API_KEY': ('shodan', 'api_key'),
|
|
248
|
+
'CENSYS_API_ID': ('censys', 'api_id'),
|
|
249
|
+
'CENSYS_API_SECRET': ('censys', 'api_secret'),
|
|
250
|
+
'NVD_API_KEY': ('nvd', 'api_key'),
|
|
251
|
+
'VIRUSTOTAL_API_KEY': ('virustotal', 'api_key'),
|
|
252
|
+
'GREYNOISE_API_KEY': ('greynoise', 'api_key'),
|
|
253
|
+
'ABUSE_IPDB_API_KEY': ('abuse_ipdb', 'api_key'),
|
|
254
|
+
'OPENAI_API_KEY': ('openai', 'api_key'),
|
|
255
|
+
}
|
|
256
|
+
for env_var, (section, key) in env_map.items():
|
|
257
|
+
val = os.environ.get(env_var, '').strip()
|
|
258
|
+
if val:
|
|
259
|
+
entries = _normalise_provider(cfg.get(section, []))
|
|
260
|
+
if entries:
|
|
261
|
+
entries[0][key] = val
|
|
262
|
+
else:
|
|
263
|
+
entries = [{'label': 'env', key: val}]
|
|
264
|
+
cfg[section] = entries
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ── Credential accessors ──────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
def _get_first_key(section: str, key: str) -> str:
|
|
270
|
+
"""Return the first non-empty value of *key* from the provider list."""
|
|
271
|
+
cfg = _config or load_config()
|
|
272
|
+
entries = _normalise_provider(cfg.get(section, []))
|
|
273
|
+
for entry in entries:
|
|
274
|
+
val = str(entry.get(key, '')).strip()
|
|
275
|
+
if val:
|
|
276
|
+
return val
|
|
277
|
+
return ''
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _get_first_entry(section: str) -> Dict:
|
|
281
|
+
"""Return the first non-empty credential entry for a provider section."""
|
|
282
|
+
cfg = _config or load_config()
|
|
283
|
+
entries = _normalise_provider(cfg.get(section, []))
|
|
284
|
+
for entry in entries:
|
|
285
|
+
# An entry is "valid" if at least one non-label key is non-empty
|
|
286
|
+
has_value = any(v for k, v in entry.items()
|
|
287
|
+
if k not in ('label', '_comment') and str(v).strip())
|
|
288
|
+
if has_value:
|
|
289
|
+
return entry
|
|
290
|
+
return {}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get(section: str, key: str, default: Any = None) -> Any:
|
|
294
|
+
"""Get a value from a flat config section (network, ml, output, etc.)."""
|
|
295
|
+
cfg = _config or load_config()
|
|
296
|
+
sec = cfg.get(section, {})
|
|
297
|
+
if isinstance(sec, dict):
|
|
298
|
+
return sec.get(key, default)
|
|
299
|
+
# Provider list — return first key
|
|
300
|
+
return _get_first_key(section, key) or default
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def get_section(section: str) -> dict:
|
|
304
|
+
"""Return a flat config section as a dict."""
|
|
305
|
+
cfg = _config or load_config()
|
|
306
|
+
sec = cfg.get(section, {})
|
|
307
|
+
return dict(sec) if isinstance(sec, dict) else {}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# ── Feature availability ──────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
def feature_available(feature: str) -> bool:
|
|
313
|
+
"""
|
|
314
|
+
Return True if the required credentials for *feature* are configured.
|
|
315
|
+
|
|
316
|
+
Example::
|
|
317
|
+
if not feature_available('shodan_search'):
|
|
318
|
+
print(warn_missing('shodan_search'))
|
|
319
|
+
"""
|
|
320
|
+
req = FEATURE_REQUIREMENTS.get(feature)
|
|
321
|
+
if not req:
|
|
322
|
+
return False
|
|
323
|
+
section = req['section']
|
|
324
|
+
keys = req['keys']
|
|
325
|
+
entry = _get_first_entry(section)
|
|
326
|
+
return all(str(entry.get(k, '')).strip() for k in keys)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def warn_missing(feature: str, verbose: bool = True) -> str:
|
|
330
|
+
"""
|
|
331
|
+
Return a formatted warning string for a missing credential.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
feature: Feature name from FEATURE_REQUIREMENTS.
|
|
335
|
+
verbose: If True, include the URL to get credentials.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
A human-readable warning string.
|
|
339
|
+
"""
|
|
340
|
+
req = FEATURE_REQUIREMENTS.get(feature)
|
|
341
|
+
if not req:
|
|
342
|
+
return f"[!] Unknown feature: {feature}"
|
|
343
|
+
|
|
344
|
+
label = req['label']
|
|
345
|
+
section = req['section']
|
|
346
|
+
keys = req['keys']
|
|
347
|
+
url = req.get('url', '')
|
|
348
|
+
optional = req.get('optional', False)
|
|
349
|
+
|
|
350
|
+
missing = [k for k in keys if not str(_get_first_entry(section).get(k, '')).strip()]
|
|
351
|
+
kind = "optional enhancement" if optional else "required credential"
|
|
352
|
+
msg = (f"[!] Feature unavailable: {label}\n"
|
|
353
|
+
f" Missing {kind}: config.json → {section}[].{', '.join(missing)}")
|
|
354
|
+
if verbose and url:
|
|
355
|
+
msg += f"\n Get credentials: {url}"
|
|
356
|
+
return msg
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def check_all_features(print_report: bool = True) -> Dict[str, bool]:
|
|
360
|
+
"""
|
|
361
|
+
Check availability of all known features.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
print_report: If True, print a formatted availability table to stdout.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Dict of {feature_name: bool}.
|
|
368
|
+
"""
|
|
369
|
+
status = {f: feature_available(f) for f in FEATURE_REQUIREMENTS}
|
|
370
|
+
|
|
371
|
+
if print_report:
|
|
372
|
+
cfg_src = str(_config_path) if _config_path else 'no config file (defaults only)'
|
|
373
|
+
print(f"\n Config: {cfg_src}")
|
|
374
|
+
print(f"\n {'Feature':<35} {'Status'}")
|
|
375
|
+
print(f" {'-'*60}")
|
|
376
|
+
for feat, avail in status.items():
|
|
377
|
+
req = FEATURE_REQUIREMENTS[feat]
|
|
378
|
+
opt = ' (optional)' if req.get('optional') else ''
|
|
379
|
+
icon = '\033[0;32mOK\033[0m' if avail else '\033[1;33mNO KEY\033[0m'
|
|
380
|
+
print(f" {req['label']:<35} {icon}{opt}")
|
|
381
|
+
print()
|
|
382
|
+
|
|
383
|
+
return status
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def require_feature(feature: str, exit_on_missing: bool = False) -> bool:
|
|
387
|
+
"""
|
|
388
|
+
Assert a feature is available; warn if not.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
feature: Feature name from FEATURE_REQUIREMENTS.
|
|
392
|
+
exit_on_missing: If True, raise SystemExit when feature is missing.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
True if available, False otherwise (after printing a warning).
|
|
396
|
+
"""
|
|
397
|
+
if feature_available(feature):
|
|
398
|
+
return True
|
|
399
|
+
|
|
400
|
+
req = FEATURE_REQUIREMENTS.get(feature, {})
|
|
401
|
+
if req.get('optional'):
|
|
402
|
+
_log.info(warn_missing(feature, verbose=False))
|
|
403
|
+
else:
|
|
404
|
+
print(warn_missing(feature, verbose=True))
|
|
405
|
+
|
|
406
|
+
if exit_on_missing:
|
|
407
|
+
raise SystemExit(1)
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# ── Convenience accessors ─────────────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
def shodan_key() -> str:
|
|
414
|
+
"""Return the first non-empty Shodan API key."""
|
|
415
|
+
return _get_first_key('shodan', 'api_key')
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def censys_credentials() -> Tuple[str, str]:
|
|
419
|
+
"""Return (api_id, api_secret) for Censys."""
|
|
420
|
+
entry = _get_first_entry('censys')
|
|
421
|
+
return (str(entry.get('api_id', '')).strip(),
|
|
422
|
+
str(entry.get('api_secret', '')).strip())
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def fofa_key() -> str:
|
|
426
|
+
"""Return the FOFA API key.
|
|
427
|
+
|
|
428
|
+
FOFA deprecated email-based auth in December 2023; only the API key is now required.
|
|
429
|
+
"""
|
|
430
|
+
return _get_first_key('fofa', 'api_key')
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def fofa_credentials() -> Tuple[str, str]:
|
|
434
|
+
"""Return (email, api_key) for FOFA — kept for backwards compatibility.
|
|
435
|
+
|
|
436
|
+
The email field is deprecated since December 2023. Callers should prefer
|
|
437
|
+
``fofa_key()`` directly.
|
|
438
|
+
"""
|
|
439
|
+
entry = _get_first_entry('fofa')
|
|
440
|
+
return (str(entry.get('email', '')).strip(),
|
|
441
|
+
str(entry.get('api_key', '')).strip())
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def zoomeye_key() -> str:
|
|
445
|
+
"""Return the ZoomEye API key."""
|
|
446
|
+
return _get_first_key('zoomeye', 'api_key')
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def netlas_key() -> str:
|
|
450
|
+
"""Return the Netlas API key."""
|
|
451
|
+
return _get_first_key('netlas', 'api_key')
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def nvd_key() -> str:
|
|
455
|
+
"""Return the NVD API key (empty string = use public rate limit)."""
|
|
456
|
+
return _get_first_key('nvd', 'api_key')
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def openai_key() -> str:
|
|
460
|
+
"""Return the first non-empty OpenAI API key."""
|
|
461
|
+
return _get_first_key('openai', 'api_key')
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def openai_model() -> str:
|
|
465
|
+
"""Return the configured OpenAI model name."""
|
|
466
|
+
entry = _get_first_entry('openai')
|
|
467
|
+
return str(entry.get('model', 'gpt-4o')).strip() or 'gpt-4o'
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def anthropic_key() -> str:
|
|
471
|
+
"""Return the Anthropic Claude API key."""
|
|
472
|
+
return _get_first_key('anthropic', 'api_key')
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def anthropic_model() -> str:
|
|
476
|
+
"""Return the configured Anthropic Claude model name."""
|
|
477
|
+
entry = _get_first_entry('anthropic')
|
|
478
|
+
return str(entry.get('model', 'claude-opus-4-5')).strip() or 'claude-opus-4-5'
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def gemini_key() -> str:
|
|
482
|
+
"""Return the first non-empty Google Gemini API key."""
|
|
483
|
+
return _get_first_key('gemini', 'api_key')
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def gemini_model() -> str:
|
|
487
|
+
"""Return the configured Gemini model name."""
|
|
488
|
+
entry = _get_first_entry('gemini')
|
|
489
|
+
return str(entry.get('model', 'gemini-2.0-flash')).strip() or 'gemini-2.0-flash'
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def active_llm() -> Tuple[str, str, str]:
|
|
493
|
+
"""
|
|
494
|
+
Return (provider, api_key, model) for the first configured LLM.
|
|
495
|
+
|
|
496
|
+
Priority: openai → anthropic → gemini.
|
|
497
|
+
Returns ('none', '', '') if no LLM is configured.
|
|
498
|
+
"""
|
|
499
|
+
for provider, key_fn, model_fn in [
|
|
500
|
+
('openai', openai_key, openai_model),
|
|
501
|
+
('anthropic', anthropic_key, anthropic_model),
|
|
502
|
+
('gemini', gemini_key, gemini_model),
|
|
503
|
+
]:
|
|
504
|
+
key = key_fn()
|
|
505
|
+
if key:
|
|
506
|
+
return (provider, key, model_fn())
|
|
507
|
+
return ('none', '', '')
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def ml_enabled() -> bool:
|
|
511
|
+
"""Return True if the ML engine is enabled in config."""
|
|
512
|
+
return bool(get('ml', 'enabled', False))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def network_timeout() -> float:
|
|
516
|
+
"""Return the configured network timeout in seconds."""
|
|
517
|
+
return float(get('network', 'timeout', 5))
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def config_path() -> Optional[Path]:
|
|
521
|
+
"""Return the path of the loaded config file, or None."""
|
|
522
|
+
return _config_path
|
src/utils/cve_loader.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CVE Catalog Loader — PrinterXPL-Forge
|
|
3
|
+
Loads the local cve_catalog.json for offline lookup; falls back to NVD API for unknown CVEs.
|
|
4
|
+
|
|
5
|
+
Author: Andre Henrique (@mrhenrike) | Uniao Geek — https://github.com/Uniao-Geek
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import urllib.request
|
|
14
|
+
import urllib.error
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
_CATALOG_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "cve_catalog.json")
|
|
18
|
+
_NVD_API = "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={}"
|
|
19
|
+
_catalog: Optional[dict] = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_catalog() -> dict:
|
|
23
|
+
global _catalog
|
|
24
|
+
if _catalog is None:
|
|
25
|
+
try:
|
|
26
|
+
with open(os.path.realpath(_CATALOG_PATH), encoding="utf-8") as f:
|
|
27
|
+
_catalog = json.load(f)
|
|
28
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
29
|
+
_catalog = {"cves": []}
|
|
30
|
+
return _catalog
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_all() -> list[dict]:
|
|
34
|
+
"""Return all CVE entries from the local catalog."""
|
|
35
|
+
return _load_catalog().get("cves", [])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def lookup(cve_id: str) -> Optional[dict]:
|
|
39
|
+
"""
|
|
40
|
+
Look up a CVE by ID (e.g. 'CVE-2024-47176').
|
|
41
|
+
Returns local catalog entry if found, else queries NVD API.
|
|
42
|
+
"""
|
|
43
|
+
cve_id = cve_id.upper().strip()
|
|
44
|
+
for entry in get_all():
|
|
45
|
+
if entry.get("id", "").upper() == cve_id:
|
|
46
|
+
return entry
|
|
47
|
+
return _nvd_lookup(cve_id)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def search(query: str, fields: Optional[list[str]] = None) -> list[dict]:
|
|
51
|
+
"""
|
|
52
|
+
Full-text search across CVE entries.
|
|
53
|
+
Optionally restrict search to specific fields (e.g. ['vendor', 'product']).
|
|
54
|
+
"""
|
|
55
|
+
q = query.lower()
|
|
56
|
+
results = []
|
|
57
|
+
search_fields = fields or ["id", "vendor", "product", "description", "attack_type", "protocol"]
|
|
58
|
+
for entry in get_all():
|
|
59
|
+
for field in search_fields:
|
|
60
|
+
val = str(entry.get(field, "")).lower()
|
|
61
|
+
if q in val:
|
|
62
|
+
results.append(entry)
|
|
63
|
+
break
|
|
64
|
+
return results
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def filter_by(
|
|
68
|
+
vendor: Optional[str] = None,
|
|
69
|
+
attack_type: Optional[str] = None,
|
|
70
|
+
protocol: Optional[str] = None,
|
|
71
|
+
min_cvss: float = 0.0,
|
|
72
|
+
port: Optional[int] = None,
|
|
73
|
+
poc_only: bool = False,
|
|
74
|
+
) -> list[dict]:
|
|
75
|
+
"""Filter CVE catalog by vendor, attack type, protocol, CVSS score, port, or PoC availability."""
|
|
76
|
+
results = []
|
|
77
|
+
for entry in get_all():
|
|
78
|
+
if vendor and vendor.lower() not in entry.get("vendor", "").lower():
|
|
79
|
+
continue
|
|
80
|
+
if attack_type and attack_type.lower() not in entry.get("attack_type", "").lower():
|
|
81
|
+
continue
|
|
82
|
+
if protocol and protocol.lower() not in entry.get("protocol", "").lower():
|
|
83
|
+
continue
|
|
84
|
+
if entry.get("cvss", 0.0) < min_cvss:
|
|
85
|
+
continue
|
|
86
|
+
if port is not None and entry.get("port") != port:
|
|
87
|
+
continue
|
|
88
|
+
if poc_only and not entry.get("poc_available", False):
|
|
89
|
+
continue
|
|
90
|
+
results.append(entry)
|
|
91
|
+
return results
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_by_xpl_module(module_path: str) -> list[dict]:
|
|
95
|
+
"""Return all CVEs that have a specific xpl module assigned."""
|
|
96
|
+
return [e for e in get_all() if e.get("xpl_module") == module_path]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def catalog_summary() -> dict:
|
|
100
|
+
"""Return a summary of catalog statistics."""
|
|
101
|
+
cves = get_all()
|
|
102
|
+
vendors: dict[str, int] = {}
|
|
103
|
+
attack_types: dict[str, int] = {}
|
|
104
|
+
for e in cves:
|
|
105
|
+
v = e.get("vendor", "Unknown").split("/")[0].strip()
|
|
106
|
+
vendors[v] = vendors.get(v, 0) + 1
|
|
107
|
+
a = e.get("attack_type", "Unknown").split("/")[0].strip()
|
|
108
|
+
attack_types[a] = attack_types.get(a, 0) + 1
|
|
109
|
+
return {
|
|
110
|
+
"total": len(cves),
|
|
111
|
+
"vendors": vendors,
|
|
112
|
+
"attack_types": attack_types,
|
|
113
|
+
"with_poc": sum(1 for e in cves if e.get("poc_available")),
|
|
114
|
+
"with_xpl_module": sum(1 for e in cves if e.get("xpl_module")),
|
|
115
|
+
"with_msf_module": sum(1 for e in cves if e.get("msf_module")),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _nvd_lookup(cve_id: str) -> Optional[dict]:
|
|
120
|
+
"""Query the NVD API for a CVE not in the local catalog."""
|
|
121
|
+
if not re.match(r"^CVE-\d{4}-\d{4,}$", cve_id, re.I):
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
url = _NVD_API.format(cve_id)
|
|
125
|
+
req = urllib.request.Request(url, headers={"User-Agent": "PrinterXPL-Forge/4.0"})
|
|
126
|
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
127
|
+
data = json.load(resp)
|
|
128
|
+
items = data.get("vulnerabilities", [])
|
|
129
|
+
if not items:
|
|
130
|
+
return None
|
|
131
|
+
nvd = items[0].get("cve", {})
|
|
132
|
+
desc = ""
|
|
133
|
+
for d in nvd.get("descriptions", []):
|
|
134
|
+
if d.get("lang") == "en":
|
|
135
|
+
desc = d.get("value", "")
|
|
136
|
+
break
|
|
137
|
+
metrics = nvd.get("metrics", {})
|
|
138
|
+
cvss = 0.0
|
|
139
|
+
for key in ("cvssMetricV31", "cvssMetricV30", "cvssMetricV2"):
|
|
140
|
+
if key in metrics and metrics[key]:
|
|
141
|
+
cvss = metrics[key][0].get("cvssData", {}).get("baseScore", 0.0)
|
|
142
|
+
break
|
|
143
|
+
return {
|
|
144
|
+
"id": cve_id,
|
|
145
|
+
"vendor": "Unknown (NVD)",
|
|
146
|
+
"product": "Unknown",
|
|
147
|
+
"attack_type": "Unknown",
|
|
148
|
+
"protocol": "Unknown",
|
|
149
|
+
"cvss": cvss,
|
|
150
|
+
"year": int(cve_id.split("-")[1]),
|
|
151
|
+
"description": desc,
|
|
152
|
+
"poc_available": False,
|
|
153
|
+
"xpl_module": None,
|
|
154
|
+
"msf_module": None,
|
|
155
|
+
"source": "nvd_api",
|
|
156
|
+
}
|
|
157
|
+
except (urllib.error.URLError, json.JSONDecodeError, KeyError, ValueError):
|
|
158
|
+
return None
|