changedetection.io-osint-processor 0.0.1__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 (29) hide show
  1. changedetection_io_osint_processor-0.0.1.dist-info/METADATA +274 -0
  2. changedetection_io_osint_processor-0.0.1.dist-info/RECORD +29 -0
  3. changedetection_io_osint_processor-0.0.1.dist-info/WHEEL +5 -0
  4. changedetection_io_osint_processor-0.0.1.dist-info/entry_points.txt +2 -0
  5. changedetection_io_osint_processor-0.0.1.dist-info/licenses/LICENSE +661 -0
  6. changedetection_io_osint_processor-0.0.1.dist-info/top_level.txt +1 -0
  7. changedetectionio_osint/__init__.py +22 -0
  8. changedetectionio_osint/forms.py +289 -0
  9. changedetectionio_osint/plugin.py +37 -0
  10. changedetectionio_osint/processor.py +655 -0
  11. changedetectionio_osint/steps/__init__.py +4 -0
  12. changedetectionio_osint/steps/base.py +76 -0
  13. changedetectionio_osint/steps/bgp.py +88 -0
  14. changedetectionio_osint/steps/dns.py +147 -0
  15. changedetectionio_osint/steps/dns_scan.py +88 -0
  16. changedetectionio_osint/steps/dnssec.py +260 -0
  17. changedetectionio_osint/steps/email_security.py +236 -0
  18. changedetectionio_osint/steps/http_fingerprint.py +359 -0
  19. changedetectionio_osint/steps/http_scan.py +31 -0
  20. changedetectionio_osint/steps/mac_lookup.py +209 -0
  21. changedetectionio_osint/steps/os_detection.py +245 -0
  22. changedetectionio_osint/steps/portscan.py +113 -0
  23. changedetectionio_osint/steps/registry.py +49 -0
  24. changedetectionio_osint/steps/smtp_fingerprint.py +517 -0
  25. changedetectionio_osint/steps/ssh_fingerprint.py +310 -0
  26. changedetectionio_osint/steps/tls_analysis.py +332 -0
  27. changedetectionio_osint/steps/traceroute.py +127 -0
  28. changedetectionio_osint/steps/whois_lookup.py +125 -0
  29. changedetectionio_osint/steps/whois_scan.py +123 -0
@@ -0,0 +1,655 @@
1
+ """
2
+ OSINT Reconnaissance Processor - Configurable Execution Mode
3
+
4
+ Extends text_json_diff for comprehensive reconnaissance using:
5
+ - dnspython: Fast DNS queries
6
+ - python-whois: WHOIS lookups
7
+ - requests: HTTP response fingerprinting
8
+ - SSLyze: SSL/TLS certificate analysis
9
+ - asyncio: Fast parallel port scanning
10
+ - Traceroute: Network path analysis
11
+ - BGP: AS/routing information
12
+
13
+ Execution mode is configurable via MODE setting ("serial" or "parallel").
14
+ All DNS operations use the configured DNS_SERVER.
15
+ """
16
+
17
+ import asyncio
18
+ import ipaddress
19
+ import socket
20
+ from loguru import logger
21
+ from urllib.parse import urlparse
22
+ from requests.structures import CaseInsensitiveDict
23
+ from blinker import signal
24
+
25
+ # Import all scan step modules (alias to avoid conflicts with dnspython)
26
+ from .steps import dns as dns_step
27
+ from .steps import whois_lookup
28
+ from .steps import http_fingerprint
29
+ from .steps import tls_analysis
30
+ from .steps import portscan
31
+ from .steps import traceroute
32
+ from .steps import bgp as bgp_step
33
+ from .steps import mac_lookup
34
+ from .steps import os_detection
35
+ from .steps import email_security
36
+ from .steps import dnssec
37
+ from .steps import ssh_fingerprint
38
+ from .steps import smtp_fingerprint
39
+
40
+ # ============================================================================
41
+ # CONFIGURATION
42
+ # ============================================================================
43
+
44
+ # Execution Mode: "serial" or "parallel"
45
+ # serial: Steps run one after another (safer, easier to debug, default)
46
+ # parallel: All steps run simultaneously (faster, 4-5x speedup)
47
+ MODE = "serial"
48
+
49
+ # DNS Server Configuration
50
+ # This DNS server will be used for ALL DNS lookups
51
+ # TODO: Make this a user setting in the future
52
+ DNS_SERVER = "8.8.8.8"
53
+
54
+ # Import the text_json_diff processor to extend it
55
+ from changedetectionio.processors.text_json_diff.processor import perform_site_check as text_json_diff_processor
56
+
57
+ # Translation marker for extraction
58
+ def _(x): return x
59
+ name = _('OSINT Reconnaissance')
60
+ description = _('Comprehensive reconnaissance: DNS, DNSSEC, SPF/DMARC/DKIM, WHOIS, HTTP, TLS, SSH, SMTP, Ports, Traceroute, BGP')
61
+ del _
62
+ processor_weight = -50
63
+ list_badge_text = "OSINT"
64
+
65
+
66
+ class perform_site_check(text_json_diff_processor):
67
+ """
68
+ OSINT Reconnaissance processor that extends text_json_diff.
69
+
70
+ Reconnaissance steps (configurable MODE: serial or parallel):
71
+ - DNS queries (A, AAAA, MX, NS, TXT, SOA, CAA)
72
+ - DNSSEC validation (cryptographic signatures, chain of trust)
73
+ - Email security (SPF, DMARC, DKIM records)
74
+ - WHOIS lookups
75
+ - HTTP response fingerprinting (CDN/WAF detection, redirect chains)
76
+ - SSL/TLS certificate analysis
77
+ - SSH fingerprinting (banner, version, host keys, algorithms)
78
+ - SMTP fingerprinting (encryption, authentication, capabilities)
79
+ - Port scanning
80
+ - Traceroute (last N hops)
81
+ - BGP/ASN information
82
+
83
+ All DNS operations use the configured DNS_SERVER for consistent resolution.
84
+
85
+ Configuration:
86
+ - MODE = "serial" (default) or "parallel" (4-5x faster)
87
+ - DNS_SERVER = "8.8.8.8" (used for all DNS operations)
88
+ """
89
+
90
+ async def call_browser(self, preferred_proxy_id=None):
91
+ """
92
+ Override the browser call to perform OSINT reconnaissance.
93
+
94
+ Runs scan steps in serial (default) or parallel mode based on MODE setting.
95
+ Serial: Steps run sequentially, safer and easier to debug
96
+ Parallel: Steps run concurrently, 4-5x faster
97
+ """
98
+ url = self.watch.link
99
+ watch_uuid = self.watch.get('uuid')
100
+
101
+ # Signal for status updates to UI
102
+ update_signal = signal('watch_small_status_comment')
103
+
104
+ # Load processor-specific configuration from watch data directory
105
+ processor_config = self.get_extra_watch_config('osint_recon.json')
106
+
107
+ # Get configuration values (with defaults)
108
+ scan_mode = processor_config.get('scan_mode', MODE)
109
+ dns_server = processor_config.get('dns_server', DNS_SERVER) or DNS_SERVER
110
+
111
+ # Get enabled/disabled scan steps (all enabled by default)
112
+ enable_dns = processor_config.get('enable_dns', True)
113
+ enable_whois = processor_config.get('enable_whois', True)
114
+ enable_http = processor_config.get('enable_http', True)
115
+ enable_tls = processor_config.get('enable_tls', True)
116
+ tls_vulnerability_scan = processor_config.get('tls_vulnerability_scan', False)
117
+ enable_portscan = processor_config.get('enable_portscan', False)
118
+ enable_traceroute = processor_config.get('enable_traceroute', True)
119
+ enable_bgp = processor_config.get('enable_bgp', True)
120
+ enable_os_detection = processor_config.get('enable_os_detection', True)
121
+ enable_email_security = processor_config.get('enable_email_security', True)
122
+ enable_dnssec = processor_config.get('enable_dnssec', True)
123
+ enable_ssh = processor_config.get('enable_ssh', True)
124
+ enable_smtp = processor_config.get('enable_smtp', True)
125
+ smtp_ehlo_hostname = processor_config.get('smtp_ehlo_hostname', 'localhost.localdomain') or 'localhost.localdomain'
126
+ whois_expire_warning_days = processor_config.get('whois_expire_warning_days', 3)
127
+ tls_expire_warning_days = processor_config.get('tls_expire_warning_days', 3)
128
+
129
+ logger.info(f"Running OSINT reconnaissance on {url} (mode: {scan_mode}, DNS: {dns_server})")
130
+ update_signal.send(watch_uuid=watch_uuid, status="Starting")
131
+
132
+ # Validate URL
133
+ if not url or not url.startswith(('http://', 'https://')):
134
+ raise ValueError(f"Invalid URL for OSINT: {url}")
135
+
136
+ # Parse URL to get hostname
137
+ parsed = urlparse(url)
138
+ hostname = parsed.netloc or parsed.hostname
139
+ if not hostname:
140
+ raise ValueError(f"Could not extract hostname from URL: {url}")
141
+
142
+ port = parsed.port or (443 if parsed.scheme == 'https' else 80)
143
+
144
+ # Get proxy configuration if available
145
+ proxy_url = None
146
+ if preferred_proxy_id:
147
+ proxy_url = self.datastore.proxy_list.get(preferred_proxy_id).get('url')
148
+
149
+ # OSINT plugin ONLY supports SOCKS5 proxies
150
+ # HTTP/HTTPS proxies don't work for raw socket operations (SSH, SMTP, port scan)
151
+ if proxy_url and proxy_url.strip(): # Check if proxy is set and not empty
152
+ if not proxy_url.lower().startswith('socks5://'):
153
+ raise ValueError(
154
+ f"OSINT Reconnaissance processor only supports SOCKS5 proxies. "
155
+ f"Got '{proxy_url}' but expected 'socks5://...'. "
156
+ f"HTTP/HTTPS proxies are not supported for raw socket operations (SSH, SMTP, port scanning). "
157
+ f"Please configure a SOCKS5 proxy or use 'No proxy' for this watch."
158
+ )
159
+ logger.info(f"Using SOCKS5 proxy '{proxy_url}' for OSINT reconnaissance")
160
+
161
+ try:
162
+ import dns.resolver
163
+ import dns.reversename
164
+
165
+ # Create DNS resolver with configured DNS server
166
+ dns_resolver = dns.resolver.Resolver()
167
+ dns_resolver.nameservers = [dns_server]
168
+ logger.debug(f"Using DNS server: {dns_server} for all operations")
169
+
170
+ # Resolve IP address first (needed by several steps)
171
+ # Check if hostname is already an IP address
172
+ try:
173
+ ipaddress.ip_address(hostname)
174
+ # It's already an IP address, no DNS resolution needed
175
+ ip_address = hostname
176
+ logger.debug(f"Input is already an IP address: {ip_address}")
177
+ except ValueError:
178
+ # Not an IP address, needs DNS resolution
179
+ try:
180
+ answers = dns_resolver.resolve(hostname, 'A')
181
+ ip_address = str(answers[0])
182
+ except:
183
+ # Fallback to AAAA if no A record
184
+ try:
185
+ answers = dns_resolver.resolve(hostname, 'AAAA')
186
+ ip_address = str(answers[0])
187
+ except:
188
+ raise ValueError(f"Could not resolve hostname: {hostname}")
189
+
190
+ logger.debug(f"Resolved {hostname} to {ip_address}")
191
+
192
+ # Reverse DNS lookup
193
+ try:
194
+ rev_name = dns.reversename.from_address(ip_address)
195
+ answers = dns_resolver.resolve(rev_name, 'PTR')
196
+ reverse_dns = str(answers[0])
197
+ except:
198
+ reverse_dns = "No PTR record"
199
+
200
+ # =================================================================
201
+ # RUN SCAN STEPS (SERIAL OR PARALLEL BASED ON MODE)
202
+ # =================================================================
203
+
204
+ # Track which steps are skipped due to SOCKS5 incompatibility
205
+ skipped_steps = []
206
+ using_socks5_proxy = bool(proxy_url and proxy_url.strip())
207
+
208
+ if scan_mode == "parallel":
209
+ logger.info("Starting PARALLEL reconnaissance scans...")
210
+
211
+ # DNS must run first if SMTP is enabled (needs MX records)
212
+ # Run DNS first, then everything else in parallel
213
+ # Check SOCKS5 compatibility
214
+ if enable_dns:
215
+ if using_socks5_proxy and not dns_step.supports_socks5:
216
+ logger.info(f"Skipping DNS scan - does not support SOCKS5 proxy")
217
+ skipped_steps.append(("DNS Records", "DNS queries not compatible with SOCKS5"))
218
+ dns_results = None
219
+ else:
220
+ dns_results = await dns_step.scan_dns(hostname, dns_resolver, proxy_url, watch_uuid, update_signal)
221
+ else:
222
+ dns_results = None
223
+
224
+ # Get MX records for SMTP scanning
225
+ mx_records = []
226
+ if dns_results and isinstance(dns_results, dict):
227
+ mx_records = dns_results.get('MX', [])
228
+
229
+ # Build list of remaining scans to run in parallel
230
+ # Check SOCKS5 compatibility for each scan
231
+ scans = []
232
+
233
+ # WHOIS
234
+ if enable_whois:
235
+ if using_socks5_proxy and not whois_lookup.supports_socks5:
236
+ skipped_steps.append(("WHOIS Lookup", "WHOIS protocol (port 43) not compatible with SOCKS5"))
237
+ else:
238
+ scans.append(whois_lookup.scan_whois(hostname, watch_uuid, update_signal))
239
+
240
+ # HTTP Fingerprinting
241
+ if enable_http:
242
+ if using_socks5_proxy and not http_fingerprint.supports_socks5:
243
+ skipped_steps.append(("HTTP Fingerprinting", "HTTP client does not support SOCKS5"))
244
+ else:
245
+ scans.append(http_fingerprint.scan_http(url, dns_resolver, proxy_url, watch_uuid, update_signal))
246
+
247
+ # TLS Analysis
248
+ if enable_tls and parsed.scheme == 'https':
249
+ if using_socks5_proxy and not tls_analysis.supports_socks5:
250
+ skipped_steps.append(("SSL/TLS Analysis", "Direct TLS connections not compatible with SOCKS5"))
251
+ else:
252
+ scans.append(tls_analysis.scan_tls(hostname, port, watch_uuid, update_signal, tls_vulnerability_scan))
253
+ elif enable_tls:
254
+ scans.append(asyncio.sleep(0)) # Placeholder for TLS when not HTTPS
255
+
256
+ # Port Scanning
257
+ if enable_portscan:
258
+ if using_socks5_proxy and not portscan.supports_socks5:
259
+ skipped_steps.append(("Port Scanning", "Raw socket port scanning not compatible with SOCKS5"))
260
+ else:
261
+ scans.append(portscan.scan_ports(ip_address, None, watch_uuid, update_signal))
262
+
263
+ # Traceroute
264
+ if enable_traceroute:
265
+ if using_socks5_proxy and not traceroute.supports_socks5:
266
+ skipped_steps.append(("Traceroute", "ICMP/UDP traceroute not compatible with SOCKS5"))
267
+ else:
268
+ scans.append(traceroute.scan_traceroute(ip_address, dns_resolver, traceroute.TRACEROUTE_LAST_HOPS, watch_uuid, update_signal))
269
+
270
+ # BGP/ASN
271
+ if enable_bgp:
272
+ if using_socks5_proxy and not bgp_step.supports_socks5:
273
+ skipped_steps.append(("BGP/ASN Information", "BGP lookups not compatible with SOCKS5 proxy"))
274
+ else:
275
+ scans.append(bgp_step.scan_bgp(ip_address, watch_uuid, update_signal))
276
+
277
+ # OS Detection
278
+ if enable_os_detection:
279
+ if using_socks5_proxy and not os_detection.supports_socks5:
280
+ skipped_steps.append(("OS Detection", "TTL-based fingerprinting requires raw sockets, incompatible with SOCKS5"))
281
+ else:
282
+ scans.append(os_detection.scan_os(ip_address, watch_uuid, update_signal))
283
+
284
+ # Email Security
285
+ if enable_email_security:
286
+ if using_socks5_proxy and not email_security.supports_socks5:
287
+ skipped_steps.append(("Email Security (SPF/DMARC/DKIM)", "DNS-based queries (UDP) not compatible with SOCKS5"))
288
+ else:
289
+ scans.append(email_security.scan_email_security(hostname, dns_resolver, watch_uuid, update_signal))
290
+
291
+ # DNSSEC
292
+ if enable_dnssec:
293
+ if using_socks5_proxy and not dnssec.supports_socks5:
294
+ skipped_steps.append(("DNSSEC Validation", "DNS queries (UDP) not compatible with SOCKS5"))
295
+ else:
296
+ scans.append(dnssec.scan_dnssec(hostname, dns_resolver, watch_uuid, update_signal))
297
+
298
+ # SSH Fingerprinting
299
+ if enable_ssh:
300
+ if using_socks5_proxy and not ssh_fingerprint.supports_socks5:
301
+ skipped_steps.append(("SSH Fingerprinting", "SSH connections not compatible with SOCKS5"))
302
+ else:
303
+ scans.append(ssh_fingerprint.scan_ssh(hostname, 22, 5, proxy_url, watch_uuid, update_signal))
304
+
305
+ # SMTP Fingerprinting
306
+ if enable_smtp:
307
+ if using_socks5_proxy and not smtp_fingerprint.supports_socks5:
308
+ skipped_steps.append(("SMTP/Email Server Fingerprinting", "SMTP connections not compatible with SOCKS5"))
309
+ else:
310
+ scans.append(smtp_fingerprint.scan_smtp_mx_records(mx_records, dns_resolver, [25, 587, 465], 5, proxy_url, smtp_ehlo_hostname, watch_uuid, update_signal))
311
+
312
+ # MAC address lookup (always enabled for local network detection)
313
+ if using_socks5_proxy and not mac_lookup.supports_socks5:
314
+ skipped_steps.append(("MAC Address Lookup", "Layer 2 local network only, not compatible with SOCKS5"))
315
+ else:
316
+ scans.append(mac_lookup.scan_mac(ip_address, watch_uuid, update_signal))
317
+
318
+ # Launch all remaining scans concurrently
319
+ if scans:
320
+ results = await asyncio.gather(*scans, return_exceptions=True)
321
+
322
+ # Unpack results based on which scans were enabled
323
+ # Note: dns_results already populated above
324
+ idx = 0
325
+ whois_data = results[idx] if enable_whois else None
326
+ idx += 1 if enable_whois else 0
327
+ http_fingerprint_data = results[idx] if enable_http else None
328
+ idx += 1 if enable_http else 0
329
+ tls_results = results[idx] if enable_tls else None
330
+ idx += 1 if enable_tls else 0
331
+ open_ports = results[idx] if enable_portscan else None
332
+ idx += 1 if enable_portscan else 0
333
+ traceroute_hops = results[idx] if enable_traceroute else None
334
+ idx += 1 if enable_traceroute else 0
335
+ bgp_data = results[idx] if enable_bgp else None
336
+ idx += 1 if enable_bgp else 0
337
+ os_data = results[idx] if enable_os_detection else None
338
+ idx += 1 if enable_os_detection else 0
339
+ email_security_data = results[idx] if enable_email_security else None
340
+ idx += 1 if enable_email_security else 0
341
+ dnssec_data = results[idx] if enable_dnssec else None
342
+ idx += 1 if enable_dnssec else 0
343
+ ssh_data = results[idx] if enable_ssh else None
344
+ idx += 1 if enable_ssh else 0
345
+ smtp_data = results[idx] if enable_smtp else None
346
+ idx += 1 if enable_smtp else 0
347
+ mac_data = results[idx] # Always runs
348
+ else:
349
+ # No scans enabled
350
+ dns_results = whois_data = http_fingerprint_data = tls_results = None
351
+ open_ports = traceroute_hops = bgp_data = os_data = mac_data = None
352
+ email_security_data = dnssec_data = ssh_data = smtp_data = None
353
+
354
+ else: # scan_mode == "serial"
355
+ logger.info("Starting SERIAL reconnaissance scans...")
356
+
357
+ # Run each enabled step sequentially with SOCKS5 compatibility checks
358
+
359
+ # DNS
360
+ if enable_dns:
361
+ if using_socks5_proxy and not dns_step.supports_socks5:
362
+ skipped_steps.append(("DNS Records", "DNS queries not compatible with SOCKS5"))
363
+ dns_results = None
364
+ else:
365
+ dns_results = await dns_step.scan_dns(hostname, dns_resolver, proxy_url, watch_uuid, update_signal)
366
+ else:
367
+ dns_results = None
368
+
369
+ # Get MX records for SMTP scanning
370
+ mx_records = []
371
+ if dns_results and isinstance(dns_results, dict):
372
+ mx_records = dns_results.get('MX', [])
373
+
374
+ # WHOIS
375
+ if enable_whois:
376
+ if using_socks5_proxy and not whois_lookup.supports_socks5:
377
+ skipped_steps.append(("WHOIS Lookup", "WHOIS protocol (port 43) not compatible with SOCKS5"))
378
+ whois_data = None
379
+ else:
380
+ whois_data = await whois_lookup.scan_whois(hostname, watch_uuid, update_signal)
381
+ else:
382
+ whois_data = None
383
+
384
+ # HTTP Fingerprinting
385
+ if enable_http:
386
+ if using_socks5_proxy and not http_fingerprint.supports_socks5:
387
+ skipped_steps.append(("HTTP Fingerprinting", "HTTP client does not support SOCKS5"))
388
+ http_fingerprint_data = None
389
+ else:
390
+ http_fingerprint_data = await http_fingerprint.scan_http(url, dns_resolver, proxy_url, watch_uuid, update_signal)
391
+ else:
392
+ http_fingerprint_data = None
393
+
394
+ # TLS Analysis
395
+ if enable_tls and parsed.scheme == 'https':
396
+ if using_socks5_proxy and not tls_analysis.supports_socks5:
397
+ skipped_steps.append(("SSL/TLS Analysis", "Direct TLS connections not compatible with SOCKS5"))
398
+ tls_results = None
399
+ else:
400
+ tls_results = await tls_analysis.scan_tls(hostname, port, watch_uuid, update_signal, tls_vulnerability_scan)
401
+ else:
402
+ tls_results = None
403
+
404
+ # Port Scanning
405
+ if enable_portscan:
406
+ if using_socks5_proxy and not portscan.supports_socks5:
407
+ skipped_steps.append(("Port Scanning", "Raw socket port scanning not compatible with SOCKS5"))
408
+ open_ports = None
409
+ else:
410
+ open_ports = await portscan.scan_ports(ip_address, None, watch_uuid, update_signal)
411
+ else:
412
+ open_ports = None
413
+
414
+ # Traceroute
415
+ if enable_traceroute:
416
+ if using_socks5_proxy and not traceroute.supports_socks5:
417
+ skipped_steps.append(("Traceroute", "ICMP/UDP traceroute not compatible with SOCKS5"))
418
+ traceroute_hops = None
419
+ else:
420
+ traceroute_hops = await traceroute.scan_traceroute(ip_address, dns_resolver, traceroute.TRACEROUTE_LAST_HOPS, watch_uuid, update_signal)
421
+ else:
422
+ traceroute_hops = None
423
+
424
+ # BGP/ASN
425
+ if enable_bgp:
426
+ if using_socks5_proxy and not bgp_step.supports_socks5:
427
+ skipped_steps.append(("BGP/ASN Information", "BGP lookups not compatible with SOCKS5 proxy"))
428
+ bgp_data = None
429
+ else:
430
+ bgp_data = await bgp_step.scan_bgp(ip_address, watch_uuid, update_signal)
431
+ else:
432
+ bgp_data = None
433
+
434
+ # OS Detection
435
+ if enable_os_detection:
436
+ if using_socks5_proxy and not os_detection.supports_socks5:
437
+ skipped_steps.append(("OS Detection", "TTL-based fingerprinting requires raw sockets, incompatible with SOCKS5"))
438
+ os_data = None
439
+ else:
440
+ os_data = await os_detection.scan_os(ip_address, watch_uuid, update_signal)
441
+ else:
442
+ os_data = None
443
+
444
+ # Email Security
445
+ if enable_email_security:
446
+ if using_socks5_proxy and not email_security.supports_socks5:
447
+ skipped_steps.append(("Email Security (SPF/DMARC/DKIM)", "DNS-based queries (UDP) not compatible with SOCKS5"))
448
+ email_security_data = None
449
+ else:
450
+ email_security_data = await email_security.scan_email_security(hostname, dns_resolver, watch_uuid, update_signal)
451
+ else:
452
+ email_security_data = None
453
+
454
+ # DNSSEC
455
+ if enable_dnssec:
456
+ if using_socks5_proxy and not dnssec.supports_socks5:
457
+ skipped_steps.append(("DNSSEC Validation", "DNS queries (UDP) not compatible with SOCKS5"))
458
+ dnssec_data = None
459
+ else:
460
+ dnssec_data = await dnssec.scan_dnssec(hostname, dns_resolver, watch_uuid, update_signal)
461
+ else:
462
+ dnssec_data = None
463
+
464
+ # SSH Fingerprinting
465
+ if enable_ssh:
466
+ if using_socks5_proxy and not ssh_fingerprint.supports_socks5:
467
+ skipped_steps.append(("SSH Fingerprinting", "SSH connections not compatible with SOCKS5"))
468
+ ssh_data = None
469
+ else:
470
+ ssh_data = await ssh_fingerprint.scan_ssh(hostname, 22, 5, proxy_url, watch_uuid, update_signal)
471
+ else:
472
+ ssh_data = None
473
+
474
+ # SMTP Fingerprinting
475
+ if enable_smtp:
476
+ if using_socks5_proxy and not smtp_fingerprint.supports_socks5:
477
+ skipped_steps.append(("SMTP/Email Server Fingerprinting", "SMTP connections not compatible with SOCKS5"))
478
+ smtp_data = None
479
+ else:
480
+ smtp_data = await smtp_fingerprint.scan_smtp_mx_records(mx_records, dns_resolver, [25, 587, 465], 5, proxy_url, smtp_ehlo_hostname, watch_uuid, update_signal)
481
+ else:
482
+ smtp_data = None
483
+
484
+ # MAC address lookup (always enabled for local network detection)
485
+ if using_socks5_proxy and not mac_lookup.supports_socks5:
486
+ skipped_steps.append(("MAC Address Lookup", "Layer 2 local network only, not compatible with SOCKS5"))
487
+ mac_data = None
488
+ else:
489
+ mac_data = await mac_lookup.scan_mac(ip_address, watch_uuid, update_signal)
490
+
491
+ logger.info(f"All scans completed ({scan_mode} mode), formatting output...")
492
+
493
+ # =================================================================
494
+ # FORMAT ALL RESULTS
495
+ # =================================================================
496
+
497
+ # Build header
498
+ header_lines = []
499
+ header_lines.append(f"Target: {url}")
500
+ header_lines.append(f"Hostname: {hostname}")
501
+ header_lines.append(f"IP Address: {ip_address}")
502
+ header_lines.append(f"Reverse DNS: {reverse_dns}")
503
+
504
+ # Show proxy if configured
505
+ if proxy_url and proxy_url.strip():
506
+ header_lines.append(f"SOCKS5 Proxy: {proxy_url}")
507
+
508
+ # Add MAC address if available (local network only)
509
+ if mac_data and not isinstance(mac_data, Exception) and mac_data.get('mac_address'):
510
+ header_lines.append(f"MAC Address: {mac_data['mac_address']}")
511
+ if mac_data.get('vendor'):
512
+ header_lines.append(f"MAC Vendor: {mac_data['vendor']}")
513
+
514
+ # Show skipped steps if using SOCKS5 proxy
515
+ if skipped_steps:
516
+ header_lines.append("")
517
+ header_lines.append("⚠ STEPS SKIPPED DUE TO SOCKS5 PROXY:")
518
+ for step_name, reason in skipped_steps:
519
+ header_lines.append(f" ✗ {step_name} - {reason}")
520
+
521
+ header_lines.append("")
522
+
523
+ # Format all sections
524
+ sections = {}
525
+
526
+ # BGP/ASN section (shows first since it's network-level info)
527
+ if bgp_data and not isinstance(bgp_data, Exception):
528
+ sections["BGP / ASN Information"] = bgp_step.format_bgp_results(bgp_data)
529
+
530
+ # DNS section
531
+ if dns_results and not isinstance(dns_results, Exception):
532
+ sections["DNS Records"] = dns_step.format_dns_results(dns_results)
533
+
534
+ # DNSSEC section
535
+ if dnssec_data and not isinstance(dnssec_data, Exception):
536
+ sections["DNSSEC Validation"] = dnssec.format_dnssec_results(dnssec_data)
537
+
538
+ # Email Security section
539
+ if email_security_data and not isinstance(email_security_data, Exception):
540
+ sections["Email Security (SPF/DMARC/DKIM)"] = email_security.format_email_security_results(email_security_data)
541
+
542
+ # WHOIS section
543
+ if whois_data and not isinstance(whois_data, Exception):
544
+ sections["WHOIS Information"] = whois_lookup.format_whois_results(whois_data, whois_expire_warning_days)
545
+
546
+ # HTTP fingerprint section
547
+ if http_fingerprint_data and not isinstance(http_fingerprint_data, Exception):
548
+ sections["HTTP Response Fingerprint"] = http_fingerprint.format_http_results(http_fingerprint_data, parsed)
549
+
550
+ # TLS/SSL section (only for HTTPS)
551
+ if parsed.scheme == 'https' and tls_results and not isinstance(tls_results, Exception) and tls_results:
552
+ sections["SSL/TLS Analysis (SSLyze)"] = tls_analysis.format_tls_results(tls_results, tls_expire_warning_days, watch_uuid, update_signal)
553
+
554
+ # SSH fingerprint section
555
+ if ssh_data and not isinstance(ssh_data, Exception):
556
+ sections["SSH Server Fingerprint"] = ssh_fingerprint.format_ssh_results(ssh_data, 22)
557
+
558
+ # SMTP fingerprint section
559
+ if smtp_data and not isinstance(smtp_data, Exception):
560
+ sections["SMTP/Email Server Fingerprint"] = smtp_fingerprint.format_smtp_results(smtp_data)
561
+
562
+ # Traceroute section
563
+ if traceroute_hops and not isinstance(traceroute_hops, Exception):
564
+ sections["Traceroute (Last N Hops)"] = traceroute.format_traceroute_results(traceroute_hops)
565
+
566
+ # Port scan section
567
+ if open_ports is not None and not isinstance(open_ports, Exception):
568
+ sections["Port Scan (Common Ports)"] = portscan.format_portscan_results(open_ports)
569
+
570
+ # OS detection section
571
+ if os_data and not isinstance(os_data, Exception):
572
+ sections["Operating System Detection"] = os_detection.format_os_results(os_data)
573
+
574
+ # Combine header and sorted sections
575
+ output_parts = header_lines.copy()
576
+
577
+ # Sort sections alphabetically and append
578
+ for section_name in sorted(sections.keys()):
579
+ output_parts.append(sections[section_name])
580
+
581
+ output = '\n'.join(output_parts)
582
+
583
+ # Format output for better readability
584
+ output = self._format_output_for_diff(output, url)
585
+
586
+ # Set the content in our fetcher
587
+ self.fetcher.content = output
588
+
589
+ # Set some basic headers
590
+ if not hasattr(self.fetcher, 'headers') or self.fetcher.headers is None or not isinstance(self.fetcher.headers, dict):
591
+ self.fetcher.headers = CaseInsensitiveDict()
592
+ elif not isinstance(self.fetcher.headers, CaseInsensitiveDict):
593
+ self.fetcher.headers = CaseInsensitiveDict(self.fetcher.headers)
594
+
595
+ self.fetcher.headers['content-type'] = 'text/plain; charset=utf-8'
596
+
597
+ # Mark as successful fetch
598
+ if not hasattr(self.fetcher, 'status_code') or self.fetcher.status_code is None:
599
+ self.fetcher.status_code = 200
600
+
601
+ update_signal.send(watch_uuid=watch_uuid, status="Done")
602
+ logger.info(f"OSINT reconnaissance completed successfully, captured {len(output)} bytes")
603
+
604
+ except ImportError as e:
605
+ error_msg = (
606
+ f"Required packages not found. Please install them:\n"
607
+ f" pip install dnspython python-whois sslyze\n"
608
+ f"Error: {str(e)}"
609
+ )
610
+ logger.error(error_msg)
611
+ raise Exception(error_msg)
612
+
613
+ except Exception as e:
614
+ import traceback
615
+ logger.error(f"OSINT reconnaissance error: {str(e)}")
616
+ logger.error(f"Traceback: {traceback.format_exc()}")
617
+ raise
618
+
619
+ @staticmethod
620
+ def _format_output_for_diff(text, url):
621
+ """
622
+ Format OSINT output for better readability in diff view.
623
+ """
624
+ lines = []
625
+
626
+ # Add header
627
+ lines.append("=" * 70)
628
+ lines.append(f"OSINT Reconnaissance Report ({MODE.capitalize()} Mode)")
629
+ lines.append("=" * 70)
630
+ lines.append(f"Target URL: {url}")
631
+ lines.append("=" * 70)
632
+ lines.append("")
633
+
634
+ # Process the output
635
+ for line in text.split('\n'):
636
+ if line.startswith('==='):
637
+ # Section headers
638
+ lines.append("")
639
+ lines.append("-" * 70)
640
+ lines.append(line.replace('===', '##'))
641
+ lines.append("-" * 70)
642
+ else:
643
+ # Indent content for readability
644
+ if line and not line.startswith(('Target:', 'Hostname:', 'IP Address:')):
645
+ lines.append(f" {line}")
646
+ else:
647
+ lines.append(line)
648
+
649
+ # Add footer
650
+ lines.append("")
651
+ lines.append("=" * 70)
652
+ lines.append("End of Report")
653
+ lines.append("=" * 70)
654
+
655
+ return '\n'.join(lines)
@@ -0,0 +1,4 @@
1
+ """
2
+ OSINT reconnaissance steps
3
+ Each module represents an independent scan step that can be run in parallel
4
+ """