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
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PrinterXPL-Forge — Wordlist Loader
|
|
5
|
+
================================
|
|
6
|
+
Loads printer credential wordlists from external files.
|
|
7
|
+
No credentials are hardcoded in the Python source.
|
|
8
|
+
|
|
9
|
+
Wordlist format (user:pass per line):
|
|
10
|
+
- Lines starting with # are comments
|
|
11
|
+
- Lines matching `# ── VENDOR NAME ──` start a new vendor section
|
|
12
|
+
- Empty password: `admin:` or `admin:` (trailing colon = blank)
|
|
13
|
+
- Empty username: `:password`
|
|
14
|
+
- Both empty: `:`
|
|
15
|
+
- Token references: `admin:__SERIAL__`, `admin:__MAC6__`, `admin:__MAC12__`
|
|
16
|
+
|
|
17
|
+
Vendor section parsing example:
|
|
18
|
+
# ── HP (Hewlett-Packard) ─────────
|
|
19
|
+
admin:
|
|
20
|
+
admin:admin
|
|
21
|
+
|
|
22
|
+
Priority:
|
|
23
|
+
1. Custom wordlist (--bf-wordlist)
|
|
24
|
+
2. Default wordlist: wordlists/printer_default_creds.txt (auto-located)
|
|
25
|
+
3. Vendor-specific fallback if a line belongs to the detected vendor section
|
|
26
|
+
4. UNIVERSAL / GENERIC entries always appended
|
|
27
|
+
"""
|
|
28
|
+
# Author : Andre Henrique (@mrhenrike)
|
|
29
|
+
# GitHub : https://github.com/mrhenrike
|
|
30
|
+
# LinkedIn : https://linkedin.com/in/mrhenrike
|
|
31
|
+
# X/Twitter : https://x.com/mrhenrike
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import re
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
40
|
+
|
|
41
|
+
from utils.default_creds import Cred, SERIAL_TOKEN, MAC6_TOKEN, MAC12_TOKEN, _ALIASES
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
# ── Vendor section header regex ───────────────────────────────────────────────
|
|
46
|
+
# Matches: # ── HP (Hewlett-Packard) ───────────────────────────────────────────
|
|
47
|
+
_SECTION_RE = re.compile(r'^#\s*[-─]+\s*(.+?)\s*[-─]*\s*$')
|
|
48
|
+
|
|
49
|
+
# Sections treated as "generic" / always included
|
|
50
|
+
_GENERIC_SECTIONS = {
|
|
51
|
+
'universal', 'generic', 'universal / generic', 'common', 'general',
|
|
52
|
+
'common default serial number patterns', 'common default', '',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ── Wordlist discovery ────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def _find_default_wordlist() -> Optional[Path]:
|
|
58
|
+
"""
|
|
59
|
+
Locate the default printer_default_creds.txt wordlist.
|
|
60
|
+
|
|
61
|
+
Searches (in order):
|
|
62
|
+
1. wordlists/ relative to this file's location (src/utils/../..)
|
|
63
|
+
2. wordlists/ relative to cwd
|
|
64
|
+
3. ~/.PrinterXPL-Forge/wordlists/
|
|
65
|
+
"""
|
|
66
|
+
candidates = [
|
|
67
|
+
Path(__file__).parent.parent.parent / "wordlists" / "printer_default_creds.txt",
|
|
68
|
+
Path.cwd() / "wordlists" / "printer_default_creds.txt",
|
|
69
|
+
Path.home() / ".PrinterXPL-Forge" / "wordlists" / "printer_default_creds.txt",
|
|
70
|
+
]
|
|
71
|
+
for p in candidates:
|
|
72
|
+
if p.exists():
|
|
73
|
+
return p
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _find_snmp_wordlist() -> Optional[Path]:
|
|
78
|
+
"""Locate snmp_communities.txt wordlist."""
|
|
79
|
+
candidates = [
|
|
80
|
+
Path(__file__).parent.parent.parent / "wordlists" / "snmp_communities.txt",
|
|
81
|
+
Path.cwd() / "wordlists" / "snmp_communities.txt",
|
|
82
|
+
]
|
|
83
|
+
for p in candidates:
|
|
84
|
+
if p.exists():
|
|
85
|
+
return p
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _find_ftp_wordlist() -> Optional[Path]:
|
|
90
|
+
"""Locate ftp_creds.txt wordlist."""
|
|
91
|
+
candidates = [
|
|
92
|
+
Path(__file__).parent.parent.parent / "wordlists" / "ftp_creds.txt",
|
|
93
|
+
Path.cwd() / "wordlists" / "ftp_creds.txt",
|
|
94
|
+
]
|
|
95
|
+
for p in candidates:
|
|
96
|
+
if p.exists():
|
|
97
|
+
return p
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ── Core parser ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def _normalize_vendor_from_section(header: str) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Extract and normalise vendor key from a section header string.
|
|
106
|
+
|
|
107
|
+
Example: 'HP (Hewlett-Packard)' → 'hp'
|
|
108
|
+
"""
|
|
109
|
+
# Strip parenthetical remarks
|
|
110
|
+
clean = re.sub(r'\(.*?\)', '', header).strip().lower()
|
|
111
|
+
# Remove trailing dashes/spaces
|
|
112
|
+
clean = clean.rstrip('-─ ')
|
|
113
|
+
return clean
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_line(line: str) -> Optional[Tuple[str, Optional[str]]]:
|
|
117
|
+
"""
|
|
118
|
+
Parse a single wordlist line into (username, password).
|
|
119
|
+
|
|
120
|
+
Returns None for blank/comment lines.
|
|
121
|
+
Password == None means blank/empty password.
|
|
122
|
+
"""
|
|
123
|
+
stripped = line.strip()
|
|
124
|
+
if not stripped or stripped.startswith('#'):
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
if ':' not in stripped:
|
|
128
|
+
# Bare word → treat as username with blank password
|
|
129
|
+
return (stripped, None)
|
|
130
|
+
|
|
131
|
+
# Split on FIRST colon only
|
|
132
|
+
idx = stripped.index(':')
|
|
133
|
+
username = stripped[:idx]
|
|
134
|
+
pwd_raw = stripped[idx + 1:]
|
|
135
|
+
|
|
136
|
+
password: Optional[str] = pwd_raw if pwd_raw else None
|
|
137
|
+
return (username, password)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_wordlist(
|
|
141
|
+
path: Optional[str | Path] = None,
|
|
142
|
+
protocol: str = 'any',
|
|
143
|
+
) -> List[Cred]:
|
|
144
|
+
"""
|
|
145
|
+
Load all credentials from a wordlist file.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
path: Path to wordlist file. If None, auto-finds the default.
|
|
149
|
+
protocol: Protocol tag to assign to all loaded Cred entries.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of Cred objects (may be empty if file not found).
|
|
153
|
+
"""
|
|
154
|
+
if path is None:
|
|
155
|
+
path = _find_default_wordlist()
|
|
156
|
+
|
|
157
|
+
if path is None:
|
|
158
|
+
logger.warning("[wordlist] Default wordlist not found; returning empty list")
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
path = Path(path)
|
|
162
|
+
if not path.exists():
|
|
163
|
+
logger.warning("[wordlist] Wordlist not found: %s", path)
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
creds: List[Cred] = []
|
|
167
|
+
seen: Set[Tuple[str, Optional[str]]] = set()
|
|
168
|
+
current_section = 'generic'
|
|
169
|
+
current_proto = protocol
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
with open(path, encoding='utf-8', errors='ignore') as fh:
|
|
173
|
+
for lineno, raw_line in enumerate(fh, 1):
|
|
174
|
+
line = raw_line.rstrip('\n\r')
|
|
175
|
+
|
|
176
|
+
# Check for section header: # ── Vendor ────
|
|
177
|
+
m = _SECTION_RE.match(line)
|
|
178
|
+
if m:
|
|
179
|
+
current_section = _normalize_vendor_from_section(m.group(1))
|
|
180
|
+
# Infer protocol from section name
|
|
181
|
+
if 'snmp' in current_section:
|
|
182
|
+
current_proto = 'snmp'
|
|
183
|
+
elif 'ftp' in current_section:
|
|
184
|
+
current_proto = 'ftp'
|
|
185
|
+
elif 'telnet' in current_section:
|
|
186
|
+
current_proto = 'telnet'
|
|
187
|
+
else:
|
|
188
|
+
current_proto = protocol
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
parsed = _parse_line(line)
|
|
192
|
+
if parsed is None:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
username, password = parsed
|
|
196
|
+
key = (username, password)
|
|
197
|
+
if key in seen:
|
|
198
|
+
continue
|
|
199
|
+
seen.add(key)
|
|
200
|
+
|
|
201
|
+
# Determine section vendor for the Cred notes field
|
|
202
|
+
creds.append(Cred(
|
|
203
|
+
username=username,
|
|
204
|
+
password=password,
|
|
205
|
+
protocol=current_proto,
|
|
206
|
+
notes=current_section,
|
|
207
|
+
))
|
|
208
|
+
|
|
209
|
+
except OSError as exc:
|
|
210
|
+
logger.error("[wordlist] Failed to read %s: %s", path, exc)
|
|
211
|
+
|
|
212
|
+
logger.debug("[wordlist] Loaded %d entries from %s", len(creds), path)
|
|
213
|
+
return creds
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def load_snmp_communities(path: Optional[str | Path] = None) -> List[str]:
|
|
217
|
+
"""
|
|
218
|
+
Load SNMP community strings from snmp_communities.txt.
|
|
219
|
+
|
|
220
|
+
Returns list of community strings (one per non-comment line).
|
|
221
|
+
"""
|
|
222
|
+
if path is None:
|
|
223
|
+
path = _find_snmp_wordlist()
|
|
224
|
+
if path is None:
|
|
225
|
+
return ["public", "private", "internal"]
|
|
226
|
+
|
|
227
|
+
communities: List[str] = []
|
|
228
|
+
seen: Set[str] = set()
|
|
229
|
+
try:
|
|
230
|
+
with open(path, encoding='utf-8', errors='ignore') as fh:
|
|
231
|
+
for line in fh:
|
|
232
|
+
s = line.strip()
|
|
233
|
+
if not s or s.startswith('#'):
|
|
234
|
+
continue
|
|
235
|
+
if s not in seen:
|
|
236
|
+
seen.add(s)
|
|
237
|
+
communities.append(s)
|
|
238
|
+
except OSError:
|
|
239
|
+
pass
|
|
240
|
+
return communities
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def load_ftp_creds(path: Optional[str | Path] = None) -> List[Cred]:
|
|
244
|
+
"""
|
|
245
|
+
Load FTP credentials from ftp_creds.txt.
|
|
246
|
+
|
|
247
|
+
Falls back to loading from the main wordlist FTP section if file not found.
|
|
248
|
+
"""
|
|
249
|
+
if path is None:
|
|
250
|
+
path = _find_ftp_wordlist()
|
|
251
|
+
if path is None:
|
|
252
|
+
# Pull from main wordlist
|
|
253
|
+
return [c for c in load_wordlist() if c.protocol in ('ftp', 'any')]
|
|
254
|
+
|
|
255
|
+
return load_wordlist(path=path, protocol='ftp')
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── Vendor-aware loader ───────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
def load_for_vendor(
|
|
261
|
+
vendor: str,
|
|
262
|
+
wordlist_path: Optional[str | Path] = None,
|
|
263
|
+
include_generic: bool = True,
|
|
264
|
+
) -> List[Cred]:
|
|
265
|
+
"""
|
|
266
|
+
Load credentials relevant to a specific vendor from a wordlist.
|
|
267
|
+
|
|
268
|
+
The wordlist is parsed into sections; credentials from the matching
|
|
269
|
+
vendor section are returned first, followed by GENERIC/UNIVERSAL entries.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
vendor: Vendor name (case-insensitive, e.g. 'epson', 'hp').
|
|
273
|
+
wordlist_path: Custom wordlist. If None, uses the default wordlist.
|
|
274
|
+
include_generic: Whether to append GENERIC/UNIVERSAL section entries.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Ordered list of Cred objects, deduplicated.
|
|
278
|
+
"""
|
|
279
|
+
if wordlist_path is None:
|
|
280
|
+
wl_path = _find_default_wordlist()
|
|
281
|
+
else:
|
|
282
|
+
wl_path = Path(wordlist_path)
|
|
283
|
+
|
|
284
|
+
if wl_path is None or not wl_path.exists():
|
|
285
|
+
logger.warning("[wordlist] Wordlist not found for vendor=%r; using empty list", vendor)
|
|
286
|
+
return []
|
|
287
|
+
|
|
288
|
+
# Normalize vendor key
|
|
289
|
+
key = vendor.lower().strip()
|
|
290
|
+
key = _ALIASES.get(key, key)
|
|
291
|
+
|
|
292
|
+
# Partial match support
|
|
293
|
+
all_keys = list(_ALIASES.values()) + list(_ALIASES.keys())
|
|
294
|
+
if key not in all_keys:
|
|
295
|
+
for alias_key in _ALIASES:
|
|
296
|
+
if alias_key in key or key in alias_key:
|
|
297
|
+
key = _ALIASES[alias_key]
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
# Parse the file into sections
|
|
301
|
+
vendor_creds: List[Cred] = []
|
|
302
|
+
generic_creds: List[Cred] = []
|
|
303
|
+
seen: Set[Tuple[str, Optional[str]]] = set()
|
|
304
|
+
|
|
305
|
+
current_section = 'generic'
|
|
306
|
+
current_proto = 'http'
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
with open(wl_path, encoding='utf-8', errors='ignore') as fh:
|
|
310
|
+
for raw_line in fh:
|
|
311
|
+
line = raw_line.rstrip('\n\r')
|
|
312
|
+
|
|
313
|
+
m = _SECTION_RE.match(line)
|
|
314
|
+
if m:
|
|
315
|
+
current_section = _normalize_vendor_from_section(m.group(1))
|
|
316
|
+
if 'snmp' in current_section:
|
|
317
|
+
current_proto = 'snmp'
|
|
318
|
+
elif 'ftp' in current_section:
|
|
319
|
+
current_proto = 'ftp'
|
|
320
|
+
elif 'telnet' in current_section:
|
|
321
|
+
current_proto = 'telnet'
|
|
322
|
+
else:
|
|
323
|
+
current_proto = 'http'
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
parsed = _parse_line(line)
|
|
327
|
+
if parsed is None:
|
|
328
|
+
continue
|
|
329
|
+
|
|
330
|
+
username, password = parsed
|
|
331
|
+
dedup_key = (username, password)
|
|
332
|
+
|
|
333
|
+
cred = Cred(username=username, password=password,
|
|
334
|
+
protocol=current_proto, notes=current_section)
|
|
335
|
+
|
|
336
|
+
# Determine if this section matches the requested vendor
|
|
337
|
+
section_key = _ALIASES.get(current_section, current_section)
|
|
338
|
+
is_vendor_match = (
|
|
339
|
+
section_key == key
|
|
340
|
+
or key in current_section
|
|
341
|
+
or current_section in key
|
|
342
|
+
or any(k in current_section for k in key.split())
|
|
343
|
+
)
|
|
344
|
+
is_generic = current_section in _GENERIC_SECTIONS
|
|
345
|
+
|
|
346
|
+
if is_vendor_match and dedup_key not in seen:
|
|
347
|
+
seen.add(dedup_key)
|
|
348
|
+
vendor_creds.append(cred)
|
|
349
|
+
elif is_generic and include_generic and dedup_key not in seen:
|
|
350
|
+
seen.add(dedup_key)
|
|
351
|
+
generic_creds.append(cred)
|
|
352
|
+
|
|
353
|
+
except OSError as exc:
|
|
354
|
+
logger.error("[wordlist] Failed to read %s: %s", wl_path, exc)
|
|
355
|
+
|
|
356
|
+
# Token entries: always add __SERIAL__, __MAC6__, __MAC12__ variants
|
|
357
|
+
# These are expanded at runtime in login_bruteforce.py
|
|
358
|
+
token_entries = [
|
|
359
|
+
Cred('admin', SERIAL_TOKEN, 'http', 'serial number as password'),
|
|
360
|
+
Cred('', SERIAL_TOKEN, 'http', 'blank user + serial'),
|
|
361
|
+
Cred('admin', MAC6_TOKEN, 'http', 'last 6 of MAC'),
|
|
362
|
+
Cred('admin', MAC12_TOKEN, 'http', 'full MAC'),
|
|
363
|
+
]
|
|
364
|
+
for tc in token_entries:
|
|
365
|
+
k = (tc.username, tc.password)
|
|
366
|
+
if k not in seen:
|
|
367
|
+
seen.add(k)
|
|
368
|
+
vendor_creds.append(tc)
|
|
369
|
+
|
|
370
|
+
result = vendor_creds + generic_creds
|
|
371
|
+
logger.debug("[wordlist] vendor=%r → %d vendor + %d generic = %d total",
|
|
372
|
+
vendor, len(vendor_creds), len(generic_creds), len(result))
|
|
373
|
+
return result
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ── Convenience: default wordlist path ───────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
def get_default_wordlist_path() -> Optional[str]:
|
|
379
|
+
"""Return the path to the default wordlist as a string, or None."""
|
|
380
|
+
p = _find_default_wordlist()
|
|
381
|
+
return str(p) if p else None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def wordlist_stats(path: Optional[str | Path] = None) -> Dict[str, int]:
|
|
385
|
+
"""
|
|
386
|
+
Return stats about a wordlist file: total entries and count per vendor section.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
path: Wordlist path. If None, uses the default wordlist.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Dict mapping section name → count of credential entries.
|
|
393
|
+
"""
|
|
394
|
+
if path is None:
|
|
395
|
+
path = _find_default_wordlist()
|
|
396
|
+
if path is None:
|
|
397
|
+
return {}
|
|
398
|
+
|
|
399
|
+
stats: Dict[str, int] = {}
|
|
400
|
+
current_section = 'generic'
|
|
401
|
+
try:
|
|
402
|
+
with open(path, encoding='utf-8', errors='ignore') as fh:
|
|
403
|
+
for line in fh:
|
|
404
|
+
m = _SECTION_RE.match(line.rstrip())
|
|
405
|
+
if m:
|
|
406
|
+
current_section = _normalize_vendor_from_section(m.group(1))
|
|
407
|
+
continue
|
|
408
|
+
if _parse_line(line) is not None:
|
|
409
|
+
stats[current_section] = stats.get(current_section, 0) + 1
|
|
410
|
+
except OSError:
|
|
411
|
+
pass
|
|
412
|
+
return stats
|
src/version.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PrinterXPL-Forge — version control module.
|
|
5
|
+
|
|
6
|
+
Versioning scheme: MAJOR.MINOR.PATCH (semver-inspired)
|
|
7
|
+
MAJOR — breaking changes or major new feature sets
|
|
8
|
+
MINOR — new features, backwards-compatible
|
|
9
|
+
PATCH — bug fixes, small improvements
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Author : Andre Henrique (@mrhenrike)
|
|
13
|
+
# GitHub : https://github.com/mrhenrike
|
|
14
|
+
# LinkedIn : https://linkedin.com/in/mrhenrike
|
|
15
|
+
# X/Twitter : https://x.com/mrhenrike
|
|
16
|
+
|
|
17
|
+
__version__ = "6.2.0"
|
|
18
|
+
__version_info__ = (6, 2, 0)
|
|
19
|
+
__release_date__ = "2026-05-12"
|
|
20
|
+
__author__ = "Andre Henrique"
|
|
21
|
+
__license__ = "MIT"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_version() -> str:
|
|
25
|
+
"""Return the bare version string, e.g. '3.0.0'."""
|
|
26
|
+
return __version__
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_version_info() -> tuple:
|
|
30
|
+
"""Return the version as a (major, minor, patch) tuple."""
|
|
31
|
+
return __version_info__
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_version_string() -> str:
|
|
35
|
+
"""Return formatted version string used in the banner."""
|
|
36
|
+
return f"Version {__version__} ({__release_date__})"
|