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.
Files changed (97) hide show
  1. nse/README.md +204 -0
  2. nse/__init__.py +6 -0
  3. nse/install_nse.py +412 -0
  4. nse/lib/printerxpl.lua +238 -0
  5. nse/scripts/cups-info.nse +74 -0
  6. nse/scripts/cups-queue-info.nse +43 -0
  7. nse/scripts/hp-printers-cve-2022-1026.nse +121 -0
  8. nse/scripts/http-device-mac.nse +107 -0
  9. nse/scripts/http-hp-ilo-info.nse +121 -0
  10. nse/scripts/http-info-xerox-enum.nse +101 -0
  11. nse/scripts/http-vuln-cve2022-1026.nse +158 -0
  12. nse/scripts/lexmark-config.nse +89 -0
  13. nse/scripts/pjl-ready-message.nse +106 -0
  14. nse/scripts/printer-banner.nse +217 -0
  15. nse/scripts/printer-cups-rce.nse +189 -0
  16. nse/scripts/printer-cve-detect.nse +279 -0
  17. nse/scripts/printer-discover.nse +205 -0
  18. nse/scripts/printer-firmware-exposed.nse +219 -0
  19. nse/scripts/printer-hp-pjl.nse +192 -0
  20. nse/scripts/printer-http-ews.nse +293 -0
  21. nse/scripts/printer-ipp-info.nse +235 -0
  22. nse/scripts/printer-lexmark-ipp.nse +203 -0
  23. nse/scripts/printer-passback.nse +204 -0
  24. nse/scripts/printer-pjl-info.nse +146 -0
  25. nse/scripts/printer-printnightmare.nse +211 -0
  26. nse/scripts/printer-snmp-info.nse +176 -0
  27. nse/scripts/printer-vuln-check.nse +256 -0
  28. nse/scripts/snmp-device-mac.nse +93 -0
  29. nse/scripts/snmp-info.nse +146 -0
  30. nse/scripts/snmp-sysdescr.nse +70 -0
  31. printerxpl_forge-6.2.0.dist-info/METADATA +919 -0
  32. printerxpl_forge-6.2.0.dist-info/RECORD +97 -0
  33. printerxpl_forge-6.2.0.dist-info/WHEEL +5 -0
  34. printerxpl_forge-6.2.0.dist-info/entry_points.txt +4 -0
  35. printerxpl_forge-6.2.0.dist-info/licenses/LICENSE +21 -0
  36. printerxpl_forge-6.2.0.dist-info/top_level.txt +4 -0
  37. src/assets/fonts/gunplay.pfa +1671 -0
  38. src/assets/fonts/kshandwrt.pfa +315 -0
  39. src/assets/fonts/laksoner.pfa +2402 -0
  40. src/assets/fonts/paintcans.pfa +9699 -0
  41. src/assets/fonts/stencilod.pfa +4076 -0
  42. src/assets/fonts/takecover.pfa +26138 -0
  43. src/assets/fonts/topsecret.pfa +6652 -0
  44. src/assets/fonts/whoa.pfa +773 -0
  45. src/assets/mibs/HOST-RESOURCES-MIB +1540 -0
  46. src/assets/mibs/Printer-MIB +4389 -0
  47. src/assets/mibs/README.md +9 -0
  48. src/assets/mibs/SNMPv2-MIB +854 -0
  49. src/assets/overlays/hacker.eps +596 -0
  50. src/assets/overlays/smiley.eps +214 -0
  51. src/assets/overlays/smiley2.eps +240 -0
  52. src/core/attack_orchestrator.py +1025 -0
  53. src/core/capabilities.py +323 -0
  54. src/core/destructive_audit.py +430 -0
  55. src/core/discovery.py +488 -0
  56. src/core/osdetect.py +74 -0
  57. src/core/poly_runner.py +579 -0
  58. src/core/printer.py +1426 -0
  59. src/main.py +2134 -0
  60. src/modules/install_printer.py +318 -0
  61. src/modules/login_bruteforce.py +852 -0
  62. src/modules/pcl.py +506 -0
  63. src/modules/pjl.py +3575 -0
  64. src/modules/print_job.py +1290 -0
  65. src/modules/ps.py +1102 -0
  66. src/payloads/__init__.py +98 -0
  67. src/payloads/assets/overlays/notice.eps +9 -0
  68. src/protocols/__init__.py +19 -0
  69. src/protocols/firmware.py +738 -0
  70. src/protocols/ipp.py +216 -0
  71. src/protocols/ipp_attacks.py +609 -0
  72. src/protocols/lpd.py +141 -0
  73. src/protocols/network_map.py +1004 -0
  74. src/protocols/raw.py +173 -0
  75. src/protocols/smb.py +359 -0
  76. src/protocols/ssrf_pivot.py +427 -0
  77. src/protocols/storage.py +587 -0
  78. src/ui/__init__.py +6 -0
  79. src/ui/interactive.py +742 -0
  80. src/ui/spinner.py +112 -0
  81. src/ui/tables.py +132 -0
  82. src/utils/banner_grabber.py +852 -0
  83. src/utils/codebook.py +456 -0
  84. src/utils/config.py +522 -0
  85. src/utils/cve_loader.py +158 -0
  86. src/utils/default_creds.py +134 -0
  87. src/utils/discovery_online.py +1327 -0
  88. src/utils/exploit_manager.py +805 -0
  89. src/utils/fuzzer.py +220 -0
  90. src/utils/helper.py +732 -0
  91. src/utils/local_printers.py +307 -0
  92. src/utils/ml_engine.py +491 -0
  93. src/utils/operators.py +474 -0
  94. src/utils/ports.py +234 -0
  95. src/utils/vuln_scanner.py +823 -0
  96. src/utils/wordlist_loader.py +412 -0
  97. 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
@@ -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