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/core/discovery.py ADDED
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ # Author : Andre Henrique (@mrhenrike)
6
+ # GitHub : https://github.com/mrhenrike
7
+ # LinkedIn : https://linkedin.com/in/mrhenrike
8
+ # X/Twitter : https://x.com/mrhenrike
9
+
10
+ import socket
11
+ import subprocess
12
+ import ipaddress
13
+ import shutil
14
+ from utils.helper import output, conv
15
+ from core.osdetect import get_os
16
+
17
+ # SNMP OIDs we’ll query
18
+ OID_HRDEV_TYPE = '1.3.6.1.2.1.25.3.2.1.2.1'
19
+ OID_HRDEV_DESCR = '1.3.6.1.2.1.25.3.2.1.3.1'
20
+ OID_SYS_UPTIME = '1.3.6.1.2.1.1.3.0'
21
+ OID_PR_STATUS = '1.3.6.1.2.1.43.16.5.1.2.1.1'
22
+ OID_PR_INTERPRETER = '1.3.6.1.2.1.43.16.5.1.2.1.2'
23
+
24
+ # HOST-RESOURCES-MIB Printer Table
25
+ OID_HRPRINTER_STATUS = '1.3.6.1.2.1.25.3.5.1.1.1'
26
+ OID_HRPRINTER_ERROR_STATE = '1.3.6.1.2.1.25.3.5.1.2.1'
27
+ OID_HRPRINTER_JOB_COUNT = '1.3.6.1.2.1.25.3.5.1.3.1'
28
+
29
+ # Printer-MIB Supplies & Alerts
30
+ OID_PRT_MARKER_SUPPLY_DESC = '1.3.6.1.2.1.43.11.1.1.6.1.1'
31
+ OID_PRT_MARKER_SUPPLY_TYPE = '1.3.6.1.2.1.43.11.1.1.2.1.1'
32
+ OID_PRT_MARKER_SUPPLY_LEVEL = '1.3.6.1.2.1.43.11.1.1.9.1.1'
33
+ OID_PRT_INPUT_MEDIA_TYPE = '1.3.6.1.2.1.43.11.1.1.2.1.1'
34
+ OID_PRT_INPUT_STATUS = '1.3.6.1.2.1.43.11.1.1.8.1.1'
35
+ OID_PRT_ALERTS_VALUE = '1.3.6.1.2.1.43.18.1.1.8.1.1'
36
+
37
+ # ENTITY-MIB Physical Entities
38
+ OID_ENT_PHYS_DESCR = '1.3.6.1.2.1.47.1.1.1.1.2'
39
+ OID_ENT_PHYS_NAME = '1.3.6.1.2.1.47.1.1.1.1.7'
40
+ OID_ENT_PHYS_FIRMWARE_REV = '1.3.6.1.2.1.47.1.1.1.1.10'
41
+ OID_ENT_PHYS_SERIAL = '1.3.6.1.2.1.47.1.1.1.1.11'
42
+ OID_ENT_PHYS_MODEL_NAME = '1.3.6.1.2.1.47.1.1.1.1.13'
43
+
44
+
45
+ def parse_selection(sel, max_index):
46
+ sel = sel.strip().lower()
47
+ if sel in ('all', 'a', ''):
48
+ return list(range(1, max_index + 1))
49
+ chosen = set()
50
+ for part in sel.split(','):
51
+ if '-' in part:
52
+ start, end = part.split('-', 1)
53
+ chosen.update(range(int(start), int(end) + 1))
54
+ else:
55
+ chosen.add(int(part))
56
+ return sorted(i for i in chosen if 1 <= i <= max_index)
57
+
58
+
59
+ def _get_local_networks(os_type):
60
+ """
61
+ Return a list of IPv4 /24 networks on UP, non-loopback,
62
+ non-link-local interfaces.
63
+
64
+ Supports: Linux, WSL, Windows, macOS (darwin), BSD, Android (Termux).
65
+ """
66
+ raw = []
67
+
68
+ # ── Linux / WSL / Android (Termux) ───────────────────────────────────
69
+ if os_type in ('linux', 'wsl', 'android'):
70
+ # Try modern iproute2 'ip' command (Linux, Android)
71
+ if shutil.which('ip'):
72
+ try:
73
+ out = subprocess.check_output(
74
+ ['ip', '-o', '-f', 'inet', 'addr', 'show'],
75
+ text=True, stderr=subprocess.DEVNULL
76
+ )
77
+ for line in out.splitlines():
78
+ parts = line.split()
79
+ if len(parts) < 4:
80
+ continue
81
+ iface = parts[1].rstrip(':')
82
+ cidr = next((p for p in parts if '/' in p), None)
83
+ if not cidr or iface in ('lo', 'lo0'):
84
+ continue
85
+ try:
86
+ raw.append(ipaddress.ip_network(cidr, strict=False))
87
+ except ValueError:
88
+ pass
89
+ except Exception as e:
90
+ output().warning(f"Could not list interfaces via 'ip': {e}")
91
+
92
+ # Fallback: ifconfig (older Linux distros, Android without ip)
93
+ if not raw and shutil.which('ifconfig'):
94
+ try:
95
+ out = subprocess.check_output(
96
+ ['ifconfig'], text=True, stderr=subprocess.DEVNULL
97
+ )
98
+ for m in __import__('re').finditer(
99
+ r'inet (?:addr:)?(\d+\.\d+\.\d+\.\d+).*?(?:Mask:|netmask )(\S+)',
100
+ out, __import__('re').DOTALL
101
+ ):
102
+ ip_str, mask_str = m.group(1), m.group(2)
103
+ try:
104
+ net = ipaddress.ip_network(f"{ip_str}/{mask_str}", strict=False)
105
+ raw.append(net)
106
+ except ValueError:
107
+ pass
108
+ except Exception as e:
109
+ output().warning(f"Could not list interfaces via 'ifconfig': {e}")
110
+
111
+ # ── macOS (darwin) ────────────────────────────────────────────────────
112
+ if os_type == 'darwin':
113
+ # networksetup gives cleaner output than ifconfig on macOS
114
+ if shutil.which('networksetup'):
115
+ try:
116
+ # Get list of network services
117
+ svcs = subprocess.check_output(
118
+ ['networksetup', '-listallnetworkservices'],
119
+ text=True, stderr=subprocess.DEVNULL
120
+ ).splitlines()[1:] # skip header line
121
+
122
+ for svc in svcs:
123
+ svc = svc.strip()
124
+ if not svc or svc.startswith('*'):
125
+ continue
126
+ try:
127
+ info = subprocess.check_output(
128
+ ['networksetup', '-getinfo', svc],
129
+ text=True, stderr=subprocess.DEVNULL
130
+ )
131
+ ip_m = __import__('re').search(r'IP address: (\d+\.\d+\.\d+\.\d+)', info)
132
+ mask_m = __import__('re').search(r'Subnet mask: (\d+\.\d+\.\d+\.\d+)', info)
133
+ if ip_m and mask_m:
134
+ ip_str, mask_str = ip_m.group(1), mask_m.group(1)
135
+ net = ipaddress.ip_network(f"{ip_str}/{mask_str}", strict=False)
136
+ raw.append(net)
137
+ except Exception:
138
+ continue
139
+ except Exception as e:
140
+ output().warning(f"Could not list macOS network services: {e}")
141
+
142
+ # Fallback: ifconfig (always present on macOS)
143
+ if not raw and shutil.which('ifconfig'):
144
+ try:
145
+ import re as _re
146
+ out = subprocess.check_output(
147
+ ['ifconfig'], text=True, stderr=subprocess.DEVNULL
148
+ )
149
+ for m in _re.finditer(
150
+ r'inet (\d+\.\d+\.\d+\.\d+) netmask (0x[0-9a-f]+|\d+\.\d+\.\d+\.\d+)',
151
+ out
152
+ ):
153
+ ip_str, mask_str = m.group(1), m.group(2)
154
+ # macOS ifconfig uses hex netmasks (e.g. 0xffffff00)
155
+ if mask_str.startswith('0x'):
156
+ mask_int = int(mask_str, 16)
157
+ mask_str = str(ipaddress.IPv4Address(mask_int))
158
+ try:
159
+ net = ipaddress.ip_network(f"{ip_str}/{mask_str}", strict=False)
160
+ raw.append(net)
161
+ except ValueError:
162
+ pass
163
+ except Exception as e:
164
+ output().warning(f"Could not list macOS interfaces via ifconfig: {e}")
165
+
166
+ # ── Windows / WSL ─────────────────────────────────────────────────────
167
+ if os_type in ('windows', 'wsl'):
168
+ pwsh = shutil.which('powershell.exe') or shutil.which('pwsh.exe')
169
+ if pwsh:
170
+ try:
171
+ cmd = [
172
+ pwsh, '-NoProfile', '-Command',
173
+ "Get-NetIPAddress -AddressFamily IPv4 "
174
+ "| Where { $_.IPAddress -ne '127.0.0.1' } "
175
+ "| Select -ExpandProperty IPAddress"
176
+ ]
177
+ out = subprocess.check_output(
178
+ cmd, text=True, stderr=subprocess.DEVNULL)
179
+ for ip in out.splitlines():
180
+ ip = ip.strip()
181
+ if not ip:
182
+ continue
183
+ try:
184
+ raw.append(ipaddress.ip_network(f"{ip}/24", strict=False))
185
+ except ValueError:
186
+ pass
187
+ except Exception as e:
188
+ output().warning(f"Could not list Windows interfaces: {e}")
189
+ else:
190
+ output().warning("PowerShell not found; skipping Windows IPs.")
191
+
192
+ # ── BSD ───────────────────────────────────────────────────────────────
193
+ if os_type == 'bsd':
194
+ if shutil.which('ifconfig'):
195
+ try:
196
+ import re as _re
197
+ out = subprocess.check_output(
198
+ ['ifconfig'], text=True, stderr=subprocess.DEVNULL
199
+ )
200
+ for m in _re.finditer(
201
+ r'inet (\d+\.\d+\.\d+\.\d+) netmask (0x[0-9a-f]+|\d+\.\d+\.\d+\.\d+)',
202
+ out
203
+ ):
204
+ ip_str, mask_str = m.group(1), m.group(2)
205
+ if mask_str.startswith('0x'):
206
+ mask_int = int(mask_str, 16)
207
+ mask_str = str(ipaddress.IPv4Address(mask_int))
208
+ try:
209
+ net = ipaddress.ip_network(f"{ip_str}/{mask_str}", strict=False)
210
+ raw.append(net)
211
+ except ValueError:
212
+ pass
213
+ except Exception as e:
214
+ output().warning(f"Could not list BSD interfaces: {e}")
215
+
216
+ # ── dedupe and filter loopback/link-local ────────────────────────────
217
+ uniq = []
218
+ for net in raw:
219
+ na = net.network_address
220
+ if na.is_loopback or na.is_link_local:
221
+ continue
222
+ if net not in uniq:
223
+ uniq.append(net)
224
+ return uniq
225
+
226
+
227
+ def _snmp_get(ip, oid):
228
+ """
229
+ Run snmpget and return the value or None.
230
+ """
231
+ cmd = shutil.which('snmpget')
232
+ if not cmd:
233
+ return None
234
+ try:
235
+ from utils.ports import PortConfig as _PC
236
+ _snmp_port = _PC.resolve('snmp')
237
+ return subprocess.check_output(
238
+ [cmd, '-v1', '-c', 'public', '-Oqv', '-t', '1', '-r', '1', f'{ip}:{_snmp_port}', oid],
239
+ stderr=subprocess.DEVNULL,
240
+ text=True
241
+ ).strip()
242
+ except subprocess.CalledProcessError:
243
+ return None
244
+
245
+
246
+ class discovery:
247
+ def __init__(self, usage=False):
248
+ os_type = get_os()
249
+ print(f"Detected OS: {os_type}")
250
+ if os_type == 'unsupported':
251
+ output().warning("This OS is not supported for SNMP-based discovery.")
252
+ return
253
+
254
+ # macOS and Android are supported – just ensure the user knows the requirements
255
+ if os_type == 'darwin':
256
+ output().chitchat("macOS detected. Discovery requires: brew install net-snmp")
257
+ if os_type == 'android':
258
+ output().chitchat("Android/Termux detected. Discovery requires: pkg install net-snmp")
259
+
260
+ if usage:
261
+ print("No target given — discovering printers on local network.")
262
+ print("Press CTRL+C at any time to cancel.\n")
263
+
264
+ if not shutil.which('snmpget'):
265
+ _install_hints = {
266
+ 'linux': "apt install snmp OR yum install net-snmp-utils",
267
+ 'wsl': "apt install snmp",
268
+ 'darwin': "brew install net-snmp",
269
+ 'bsd': "pkg install net-snmp",
270
+ 'windows': "choco install net-snmp OR use WSL",
271
+ 'android': "pkg install net-snmp (Termux)",
272
+ }
273
+ hint = _install_hints.get(os_type, "install net-snmp for your OS")
274
+ output().warning(f"'snmpget' not found. Install with: {hint}")
275
+ return
276
+
277
+ networks = _get_local_networks(os_type)
278
+ if not networks:
279
+ output().warning("No eligible networks found to scan.")
280
+ return
281
+
282
+ print(f"Found {len(networks)} network(s) to consider:")
283
+ for idx, net in enumerate(networks, 1):
284
+ hosts = net.num_addresses - 2 if net.num_addresses > 2 else net.num_addresses
285
+ print(f" [{idx}] {net} ({hosts} hosts)")
286
+
287
+ sel = input("\nSelect networks to scan [e.g. 1,1-3,all]: ")
288
+ chosen = parse_selection(sel, len(networks))
289
+ if not chosen:
290
+ print("Nothing selected, exiting.")
291
+ return
292
+
293
+ verb = input("Verbose probing? [y/N]: ").strip().lower()
294
+ verbose = verb in ('y', 'yes')
295
+
296
+ results = {}
297
+ total = 0
298
+ try:
299
+ for i in chosen:
300
+ net = networks[i - 1]
301
+ print(f"\nScanning {net} (Ctrl+C to cancel)...")
302
+ for host in net.hosts():
303
+ ip = str(host)
304
+ total += 1
305
+
306
+ typ = _snmp_get(ip, OID_HRDEV_TYPE)
307
+ if typ != '1.3.6.1.2.1.25.3.1.5':
308
+ if verbose:
309
+ print(f" {ip}: not a printer ({typ})")
310
+ continue
311
+
312
+ # collect all SNMP fields
313
+ descr = _snmp_get(ip, OID_HRDEV_DESCR) or '?'
314
+ upv = _snmp_get(ip, OID_SYS_UPTIME)
315
+ uptime = conv().elapsed(int(upv), 100, True) if upv and upv.isdigit() else '?'
316
+ pr_status = _snmp_get(ip, OID_PR_STATUS) or '?'
317
+ interp = _snmp_get(ip, OID_PR_INTERPRETER) or '?'
318
+ hp_status = _snmp_get(ip, OID_HRPRINTER_STATUS) or '?'
319
+ err_state = _snmp_get(ip, OID_HRPRINTER_ERROR_STATE) or '?'
320
+ job_count = _snmp_get(ip, OID_HRPRINTER_JOB_COUNT) or '?'
321
+ m_desc = _snmp_get(ip, OID_PRT_MARKER_SUPPLY_DESC) or '?'
322
+ m_type = _snmp_get(ip, OID_PRT_MARKER_SUPPLY_TYPE) or '?'
323
+ m_level = _snmp_get(ip, OID_PRT_MARKER_SUPPLY_LEVEL) or '?'
324
+ in_media = _snmp_get(ip, OID_PRT_INPUT_MEDIA_TYPE) or '?'
325
+ in_status = _snmp_get(ip, OID_PRT_INPUT_STATUS) or '?'
326
+ alerts = _snmp_get(ip, OID_PRT_ALERTS_VALUE) or '?'
327
+ phys_descr = _snmp_get(ip, OID_ENT_PHYS_DESCR) or '?'
328
+ phys_name = _snmp_get(ip, OID_ENT_PHYS_NAME) or '?'
329
+ phys_fw = _snmp_get(ip, OID_ENT_PHYS_FIRMWARE_REV) or '?'
330
+ phys_serial = _snmp_get(ip, OID_ENT_PHYS_SERIAL) or '?'
331
+ phys_model = _snmp_get(ip, OID_ENT_PHYS_MODEL_NAME) or '?'
332
+
333
+ results[ip] = [
334
+ descr, uptime, pr_status, interp,
335
+ hp_status, err_state, job_count,
336
+ m_desc, m_type, m_level,
337
+ in_media, in_status, alerts,
338
+ phys_name, phys_model, phys_serial, phys_fw
339
+ ]
340
+
341
+ if verbose:
342
+ print(f" {ip}:")
343
+ print(f" Description: {descr}")
344
+ print(f" Uptime: {uptime}")
345
+ print(f" PJL Status: {pr_status}")
346
+ print(f" Interpreter: {interp}")
347
+ print(f" hrStatus: {hp_status}")
348
+ print(f" Errors: {err_state}")
349
+ print(f" Jobs: {job_count}")
350
+ print(f" Supplies: {m_desc} / {m_type} @ {m_level}")
351
+ print(f" Input: {in_media} / {in_status}")
352
+ print(f" Alerts: {alerts}")
353
+ print(f" Entity: {phys_name} ({phys_model})")
354
+ print(f" Serial: {phys_serial}")
355
+ print(f" FW Rev: {phys_fw}")
356
+ else:
357
+ print(f" {ip}: Printer → {descr}, uptime={uptime}, status={pr_status}")
358
+
359
+ except KeyboardInterrupt:
360
+ print()
361
+ output().warning("[!] Discovery interrupted by user. Exiting...\n")
362
+
363
+ print(f"\nProbed {total} hosts in total.\n")
364
+ if results:
365
+ print("Discovered printers:")
366
+ hdr = (
367
+ 'address',
368
+ ('descr','uptime','pjl_status','interp','hr_status','errors',
369
+ 'jobs','sup_desc','sup_type','sup_lvl',
370
+ 'in_media','in_status','alerts',
371
+ 'ent_name','ent_model','ent_serial','ent_fw')
372
+ )
373
+ output().discover(hdr)
374
+ output().hline(79)
375
+ for entry in sorted(results.items(), key=lambda i: socket.inet_aton(i[0])):
376
+ output().discover(entry)
377
+ print()
378
+ else:
379
+ output().info("No printers found via SNMP scan")
380
+ print()
381
+
382
+
383
+ # ── Mirai-style concurrent TCP port scanner (batch-32 enhancement) ────────────
384
+ # Inspired by jgamblin/Mirai-Source-Code (scanner.c) concurrent scan approach
385
+ # and groinc (thau0x01) passive packet inspection patterns.
386
+
387
+ import concurrent.futures as _futures
388
+ import threading as _threading
389
+
390
+ _PRINTER_PROBE_PORTS = [9100, 80, 443, 631, 515, 8080, 23, 161]
391
+
392
+ def _tcp_open(host: str, port: int, timeout: float = 1.5) -> bool:
393
+ """Non-blocking TCP probe."""
394
+ try:
395
+ with socket.create_connection((host, port), timeout=timeout):
396
+ return True
397
+ except Exception:
398
+ return False
399
+
400
+
401
+ def mirai_tcp_scan(cidr: str, ports: list = None, threads: int = 256,
402
+ timeout: float = 1.5) -> list:
403
+ """
404
+ Concurrent Mirai-style TCP scanner.
405
+ Probes all hosts in `cidr` for printer-relevant ports in parallel.
406
+ Returns list of dicts: {host, open_ports}.
407
+ """
408
+ if ports is None:
409
+ ports = _PRINTER_PROBE_PORTS
410
+
411
+ try:
412
+ net = ipaddress.ip_network(cidr, strict=False)
413
+ targets = [str(ip) for ip in net.hosts()]
414
+ except ValueError as exc:
415
+ print(f"[!] Invalid CIDR {cidr}: {exc}")
416
+ return []
417
+
418
+ print(f"[*] Mirai-TCP scan: {len(targets)} hosts × {len(ports)} ports ({threads} threads)")
419
+ results = []
420
+ lock = _threading.Lock()
421
+
422
+ def _probe(host):
423
+ open_ports = [p for p in ports if _tcp_open(host, p, timeout)]
424
+ if open_ports:
425
+ with lock:
426
+ results.append({"host": host, "open_ports": open_ports})
427
+
428
+ with _futures.ThreadPoolExecutor(max_workers=threads) as pool:
429
+ list(pool.map(_probe, targets))
430
+
431
+ print(f"[*] {len(results)} host(s) with open printer ports.")
432
+ return results
433
+
434
+
435
+ def groinc_passive_sniff(interface: str = None, timeout: int = 30,
436
+ printer_ports: list = None) -> list:
437
+ """
438
+ Groinc-inspired passive packet capture to detect printer traffic.
439
+ Requires scapy (pip install scapy). Falls back gracefully if unavailable.
440
+ Detects: PJL (port 9100), IPP (port 631), LPD (port 515), SNMP (port 161).
441
+ """
442
+ if printer_ports is None:
443
+ printer_ports = [9100, 631, 515, 161]
444
+
445
+ try:
446
+ from scapy.all import sniff, TCP, UDP, IP
447
+ except ImportError:
448
+ print("[!] scapy not installed — passive sniffer unavailable.")
449
+ print(" Install with: pip install scapy")
450
+ return []
451
+
452
+ print(f"[*] Passive sniffer on {interface or 'default'} for {timeout}s "
453
+ f"(ports: {printer_ports}) ...")
454
+
455
+ seen = {}
456
+ port_filter = " or ".join(f"port {p}" for p in printer_ports)
457
+ bpf = f"tcp or udp and ({port_filter})"
458
+
459
+ def _pkt_handler(pkt):
460
+ if IP not in pkt:
461
+ return
462
+ src = pkt[IP].src
463
+ dst = pkt[IP].dst
464
+ if TCP in pkt:
465
+ dport = pkt[TCP].dport
466
+ sport = pkt[TCP].sport
467
+ elif UDP in pkt:
468
+ dport = pkt[UDP].dport
469
+ sport = pkt[UDP].sport
470
+ else:
471
+ return
472
+ for p in printer_ports:
473
+ if dport == p or sport == p:
474
+ key = (src, dst, p)
475
+ if key not in seen:
476
+ seen[key] = True
477
+ print(f" [sniff] {src}:{sport} → {dst}:{dport}")
478
+
479
+ try:
480
+ sniff(iface=interface, filter=bpf, prn=_pkt_handler,
481
+ store=False, timeout=timeout)
482
+ except Exception as exc:
483
+ print(f"[!] Sniffer error: {exc}")
484
+
485
+ flows = [{"src": k[0], "dst": k[1], "port": k[2]} for k in seen]
486
+ print(f"[*] Captured {len(flows)} unique printer flows.")
487
+ return flows
488
+
src/core/osdetect.py ADDED
@@ -0,0 +1,74 @@
1
+
2
+ # Author : Andre Henrique (@mrhenrike)
3
+ # GitHub : https://github.com/mrhenrike
4
+ # LinkedIn : https://linkedin.com/in/mrhenrike
5
+ # X/Twitter : https://x.com/mrhenrike
6
+
7
+ _cached_os = None
8
+
9
+
10
+ def get_os() -> str:
11
+ """
12
+ Detect the host operating system.
13
+
14
+ Returns one of:
15
+ 'linux' – Standard Linux
16
+ 'wsl' – Windows Subsystem for Linux
17
+ 'android' – Android via Termux (or similar)
18
+ 'windows' – Native Windows
19
+ 'darwin' – macOS
20
+ 'bsd' – FreeBSD / OpenBSD / NetBSD
21
+ 'unsupported' – anything else
22
+
23
+ Result is cached after the first call.
24
+ """
25
+ global _cached_os
26
+ if _cached_os:
27
+ return _cached_os
28
+
29
+ import platform
30
+ import os
31
+
32
+ sysname = platform.system().lower()
33
+
34
+ if "linux" in sysname:
35
+ # ── Android / Termux detection ──────────────────────────────────
36
+ # Android uses a Linux kernel but has a distinct runtime environment.
37
+ # Termux sets TERMUX_VERSION env var; alternatively check for /data/data.
38
+ if (
39
+ os.environ.get("TERMUX_VERSION")
40
+ or os.path.isdir("/data/data/com.termux")
41
+ or os.environ.get("PREFIX", "").startswith("/data/data/com.termux")
42
+ ):
43
+ _cached_os = "android"
44
+ return _cached_os
45
+
46
+ # ── WSL detection ────────────────────────────────────────────────
47
+ osrelease = '/proc/sys/kernel/osrelease'
48
+ if os.path.exists(osrelease):
49
+ try:
50
+ with open(osrelease) as f:
51
+ content = f.read().lower()
52
+ if 'microsoft' in content or 'wsl' in content:
53
+ _cached_os = "wsl"
54
+ return _cached_os
55
+ except OSError:
56
+ pass
57
+
58
+ _cached_os = "linux"
59
+ return _cached_os
60
+
61
+ if "windows" in sysname:
62
+ _cached_os = "windows"
63
+ return _cached_os
64
+
65
+ if "darwin" in sysname:
66
+ _cached_os = "darwin" # macOS
67
+ return _cached_os
68
+
69
+ if any(bsd in sysname for bsd in ("freebsd", "openbsd", "netbsd")):
70
+ _cached_os = "bsd"
71
+ return _cached_os
72
+
73
+ _cached_os = "unsupported"
74
+ return _cached_os