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,209 @@
1
+ """
2
+ MAC Address Lookup Step
3
+ Gets MAC address from ARP cache for local network targets
4
+ """
5
+
6
+ import asyncio
7
+ # SOCKS5 proxy support: MAC address lookups are local network only (Layer 2)
8
+ supports_socks5 = False
9
+ import re
10
+ import subprocess
11
+ from loguru import logger
12
+
13
+
14
+ async def scan_mac(ip_address, watch_uuid=None, update_signal=None):
15
+ """
16
+ Lookup MAC address from ARP cache
17
+
18
+ Args:
19
+ ip_address: Target IP address
20
+ watch_uuid: Optional watch UUID for status updates
21
+ update_signal: Optional blinker signal for status updates
22
+
23
+ Returns:
24
+ dict: MAC address and vendor info
25
+ """
26
+ if update_signal and watch_uuid:
27
+ update_signal.send(watch_uuid=watch_uuid, status="MAC")
28
+
29
+ def get_mac_from_arp():
30
+ """Try to get MAC address from ARP cache"""
31
+ result = {
32
+ 'mac_address': None,
33
+ 'vendor': None,
34
+ 'method': None
35
+ }
36
+
37
+ try:
38
+ # Try reading /proc/net/arp on Linux (no root needed)
39
+ try:
40
+ with open('/proc/net/arp', 'r') as f:
41
+ arp_data = f.read()
42
+
43
+ for line in arp_data.split('\n')[1:]: # Skip header
44
+ if ip_address in line:
45
+ parts = line.split()
46
+ if len(parts) >= 4:
47
+ mac = parts[3]
48
+ if mac != '00:00:00:00:00:00' and mac != '<incomplete>':
49
+ result['mac_address'] = mac.upper()
50
+ result['method'] = '/proc/net/arp'
51
+ logger.debug(f"Found MAC {mac} for {ip_address} in /proc/net/arp")
52
+ break
53
+ except (FileNotFoundError, PermissionError):
54
+ logger.debug("/proc/net/arp not available, trying arp command")
55
+
56
+ # Fallback: Try 'ip neigh' command (Linux)
57
+ if not result['mac_address']:
58
+ try:
59
+ output = subprocess.check_output(
60
+ ['ip', 'neigh', 'show', ip_address],
61
+ stderr=subprocess.DEVNULL,
62
+ timeout=2
63
+ ).decode('utf-8')
64
+
65
+ # Parse: 192.168.1.1 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE
66
+ mac_match = re.search(r'lladdr\s+([0-9a-fA-F:]{17})', output)
67
+ if mac_match:
68
+ result['mac_address'] = mac_match.group(1).upper()
69
+ result['method'] = 'ip neigh'
70
+ logger.debug(f"Found MAC via 'ip neigh': {result['mac_address']}")
71
+ except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
72
+ logger.debug("'ip neigh' command failed")
73
+
74
+ # Fallback: Try 'arp' command (Linux/Mac)
75
+ if not result['mac_address']:
76
+ try:
77
+ output = subprocess.check_output(
78
+ ['arp', '-n', ip_address],
79
+ stderr=subprocess.DEVNULL,
80
+ timeout=2
81
+ ).decode('utf-8')
82
+
83
+ # Parse: 192.168.1.1 ether aa:bb:cc:dd:ee:ff C eth0
84
+ mac_match = re.search(r'([0-9a-fA-F:]{17})', output)
85
+ if mac_match:
86
+ result['mac_address'] = mac_match.group(1).upper()
87
+ result['method'] = 'arp command'
88
+ logger.debug(f"Found MAC via 'arp': {result['mac_address']}")
89
+ except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
90
+ logger.debug("'arp' command failed")
91
+
92
+ # Lookup vendor if we found a MAC
93
+ if result['mac_address']:
94
+ result['vendor'] = lookup_mac_vendor(result['mac_address'])
95
+
96
+ except Exception as e:
97
+ logger.debug(f"MAC lookup failed: {e}")
98
+
99
+ return result
100
+
101
+ return await asyncio.to_thread(get_mac_from_arp)
102
+
103
+
104
+ def lookup_mac_vendor(mac_address):
105
+ """
106
+ Lookup MAC vendor from OUI (Organizationally Unique Identifier)
107
+
108
+ Args:
109
+ mac_address: MAC address in format AA:BB:CC:DD:EE:FF
110
+
111
+ Returns:
112
+ str: Vendor name or None
113
+ """
114
+ try:
115
+ # Try using mac-vendor-lookup library if available
116
+ try:
117
+ from mac_vendor_lookup import MacLookup
118
+ mac = MacLookup()
119
+ vendor = mac.lookup(mac_address)
120
+ return vendor
121
+ except ImportError:
122
+ logger.debug("mac-vendor-lookup not installed, using fallback")
123
+ except Exception as e:
124
+ logger.debug(f"mac-vendor-lookup failed: {e}")
125
+
126
+ # Fallback: Basic OUI lookup using common vendors
127
+ # Just first 3 bytes (OUI)
128
+ oui = mac_address[:8].replace(':', '').upper()
129
+
130
+ # Common vendor OUIs (just a small sample - full database has 40k+ entries)
131
+ common_ouis = {
132
+ '00005E': 'IANA/ICANN',
133
+ '00000C': 'Cisco Systems',
134
+ '000D3A': 'Cisco Systems',
135
+ '001A2B': 'Cisco Systems',
136
+ '00503E': 'Cisco Systems',
137
+ '001B0D': 'D-Link',
138
+ '0018E7': 'TP-Link',
139
+ '0C8268': 'TP-Link',
140
+ 'F4EC38': 'TP-Link',
141
+ '001C7F': 'NETGEAR',
142
+ '002275': 'NETGEAR',
143
+ 'A0CCEC': 'NETGEAR',
144
+ 'C83A35': 'NETGEAR',
145
+ '000C29': 'VMware',
146
+ '005056': 'VMware',
147
+ '000569': 'VMware',
148
+ '001C14': 'VMware',
149
+ '080027': 'Oracle VirtualBox',
150
+ '525400': 'QEMU Virtual NIC',
151
+ '000000': 'Xerox Corporation',
152
+ '001D7E': 'Raspberry Pi Foundation',
153
+ 'B827EB': 'Raspberry Pi Foundation',
154
+ 'DC4A3E': 'Raspberry Pi Foundation',
155
+ 'E45F01': 'Raspberry Pi Foundation',
156
+ '000A27': 'Apple',
157
+ '000D93': 'Apple',
158
+ '001124': 'Apple',
159
+ '0016CB': 'Apple',
160
+ '001E52': 'Apple',
161
+ '002332': 'Apple',
162
+ '002436': 'Apple',
163
+ '002500': 'Apple',
164
+ '0026BB': 'Apple',
165
+ '00DB70': 'Apple',
166
+ '00E091': 'Microsoft',
167
+ '000BDB': 'Microsoft',
168
+ '001DD8': 'Microsoft',
169
+ '00155D': 'Microsoft Hyper-V',
170
+ '74D435': 'Google',
171
+ 'F4F5D8': 'Google',
172
+ '3C5A37': 'Google',
173
+ '000C76': 'Intel',
174
+ '001E67': 'Intel',
175
+ '0024D7': 'Intel',
176
+ '7085C2': 'Intel',
177
+ '001B21': 'Intel',
178
+ '002170': 'Dell',
179
+ '0026B9': 'Dell',
180
+ 'D4AE52': 'Dell',
181
+ 'F04DA2': 'Dell',
182
+ '002564': 'Dell',
183
+ }
184
+
185
+ return common_ouis.get(oui[:6], 'Unknown Vendor')
186
+
187
+ except Exception as e:
188
+ logger.debug(f"Vendor lookup failed: {e}")
189
+ return None
190
+
191
+
192
+ def format_mac_results(mac_data):
193
+ """Format MAC address results for output"""
194
+ lines = []
195
+ lines.append("=== MAC Address (Local Network) ===")
196
+
197
+ if mac_data and mac_data.get('mac_address'):
198
+ lines.append(f"MAC Address: {mac_data['mac_address']}")
199
+ if mac_data.get('vendor'):
200
+ lines.append(f"Vendor: {mac_data['vendor']}")
201
+ lines.append(f"Detection Method: {mac_data.get('method', 'unknown')}")
202
+ lines.append("")
203
+ lines.append("Note: MAC address only available for local network devices")
204
+ else:
205
+ lines.append("Not available (target not in ARP cache)")
206
+ lines.append("Note: MAC addresses only visible for devices on the same local network")
207
+
208
+ lines.append("")
209
+ return '\n'.join(lines)
@@ -0,0 +1,245 @@
1
+ """
2
+ OS Detection Step
3
+ Performs operating system fingerprinting when raw socket permissions available
4
+ """
5
+
6
+ import asyncio
7
+ # SOCKS5 proxy support: OS detection requires raw sockets/ICMP
8
+ supports_socks5 = False
9
+ import socket
10
+ import struct
11
+ from loguru import logger
12
+
13
+
14
+ def check_raw_socket_permission():
15
+ """
16
+ Check if we have raw socket permissions (requires root or CAP_NET_RAW)
17
+
18
+ Returns:
19
+ bool: True if raw sockets are available
20
+ """
21
+ try:
22
+ # Try to create a raw ICMP socket
23
+ s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
24
+ s.close()
25
+ return True
26
+ except PermissionError:
27
+ return False
28
+ except OSError as e:
29
+ # Other errors (e.g., protocol not supported)
30
+ logger.debug(f"Raw socket check failed: {e}")
31
+ return False
32
+
33
+
34
+ async def scan_os(ip_address, watch_uuid=None, update_signal=None):
35
+ """
36
+ Perform OS detection/fingerprinting
37
+
38
+ Args:
39
+ ip_address: Target IP address
40
+ watch_uuid: Optional watch UUID for status updates
41
+ update_signal: Optional blinker signal for status updates
42
+
43
+ Returns:
44
+ dict: OS detection results
45
+ """
46
+ if update_signal and watch_uuid:
47
+ update_signal.send(watch_uuid=watch_uuid, status="OS Detection")
48
+
49
+ def detect_os():
50
+ result = {
51
+ 'has_raw_socket': False,
52
+ 'os_guess': None,
53
+ 'confidence': None,
54
+ 'ttl': None,
55
+ 'method': 'passive'
56
+ }
57
+
58
+ # Check for raw socket permissions
59
+ result['has_raw_socket'] = check_raw_socket_permission()
60
+
61
+ if result['has_raw_socket']:
62
+ logger.info("Raw socket access available - attempting active OS fingerprinting")
63
+ result['method'] = 'active'
64
+
65
+ # Active fingerprinting with raw sockets
66
+ try:
67
+ # Send ICMP ping and analyze TTL
68
+ ttl = get_ttl_via_ping(ip_address)
69
+ if ttl:
70
+ result['ttl'] = ttl
71
+ result['os_guess'], result['confidence'] = guess_os_from_ttl(ttl)
72
+ except Exception as e:
73
+ logger.debug(f"Active OS detection failed: {e}")
74
+ result['method'] = 'passive'
75
+ else:
76
+ logger.debug("No raw socket access - OS detection limited to passive methods")
77
+
78
+ return result
79
+
80
+ return await asyncio.to_thread(detect_os)
81
+
82
+
83
+ def get_ttl_via_ping(ip_address, timeout=2):
84
+ """
85
+ Send ICMP ping and extract TTL from response
86
+
87
+ Args:
88
+ ip_address: Target IP
89
+ timeout: Socket timeout in seconds
90
+
91
+ Returns:
92
+ int: TTL value or None
93
+ """
94
+ try:
95
+ # Create raw ICMP socket
96
+ icmp = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
97
+ icmp.settimeout(timeout)
98
+
99
+ # ICMP Echo Request packet
100
+ # Type=8 (Echo Request), Code=0, Checksum=0 (will calculate), ID=0, Seq=0
101
+ packet_id = 12345
102
+ packet_seq = 1
103
+
104
+ # Build ICMP header (type, code, checksum, id, sequence)
105
+ header = struct.pack('!BBHHH', 8, 0, 0, packet_id, packet_seq)
106
+ data = b'OSINT' * 10 # 50 bytes of data
107
+
108
+ # Calculate checksum
109
+ checksum = calculate_checksum(header + data)
110
+ header = struct.pack('!BBHHH', 8, 0, checksum, packet_id, packet_seq)
111
+
112
+ packet = header + data
113
+
114
+ # Send packet
115
+ icmp.sendto(packet, (ip_address, 0))
116
+
117
+ # Receive response
118
+ try:
119
+ reply, addr = icmp.recvfrom(1024)
120
+
121
+ # Extract TTL from IP header (9th byte)
122
+ ttl = reply[8]
123
+
124
+ icmp.close()
125
+ logger.debug(f"Received ICMP reply from {ip_address} with TTL={ttl}")
126
+ return ttl
127
+
128
+ except socket.timeout:
129
+ logger.debug(f"ICMP ping timeout for {ip_address}")
130
+ icmp.close()
131
+ return None
132
+
133
+ except Exception as e:
134
+ logger.debug(f"ICMP ping failed: {e}")
135
+ return None
136
+
137
+
138
+ def calculate_checksum(data):
139
+ """Calculate ICMP checksum"""
140
+ if len(data) % 2 == 1:
141
+ data += b'\x00'
142
+
143
+ s = sum(struct.unpack('!%dH' % (len(data) // 2), data))
144
+ s = (s >> 16) + (s & 0xffff)
145
+ s += s >> 16
146
+ s = ~s & 0xffff
147
+
148
+ return s
149
+
150
+
151
+ def guess_os_from_ttl(ttl):
152
+ """
153
+ Guess OS based on TTL value
154
+
155
+ Common initial TTL values:
156
+ - Linux/Unix: 64
157
+ - Windows: 128
158
+ - Cisco/Network devices: 255
159
+ - Some BSD: 255
160
+
161
+ Args:
162
+ ttl: TTL value from packet
163
+
164
+ Returns:
165
+ tuple: (os_guess, confidence)
166
+ """
167
+ # Calculate likely initial TTL (common values: 32, 64, 128, 255)
168
+ possible_initial_ttls = [32, 64, 128, 255]
169
+
170
+ # Find the smallest initial TTL that's >= observed TTL
171
+ initial_ttl = None
172
+ for value in possible_initial_ttls:
173
+ if ttl <= value:
174
+ initial_ttl = value
175
+ break
176
+
177
+ if not initial_ttl:
178
+ return ("Unknown (TTL too high)", "low")
179
+
180
+ hop_count = initial_ttl - ttl
181
+
182
+ # Make educated guesses based on initial TTL
183
+ if initial_ttl == 64:
184
+ # Most likely Linux/Unix
185
+ if hop_count <= 5:
186
+ return ("Linux/Unix", "high")
187
+ else:
188
+ return ("Linux/Unix", "medium")
189
+
190
+ elif initial_ttl == 128:
191
+ # Most likely Windows
192
+ if hop_count <= 5:
193
+ return ("Windows", "high")
194
+ else:
195
+ return ("Windows", "medium")
196
+
197
+ elif initial_ttl == 255:
198
+ # Could be Cisco, BSD, or other network device
199
+ if hop_count <= 5:
200
+ return ("Cisco/Network Device/BSD", "medium")
201
+ else:
202
+ return ("Cisco/Network Device/BSD", "low")
203
+
204
+ elif initial_ttl == 32:
205
+ # Less common, older Windows or some embedded systems
206
+ return ("Windows 95/98 or Embedded Device", "low")
207
+
208
+ return ("Unknown", "low")
209
+
210
+
211
+ def format_os_results(os_data):
212
+ """Format OS detection results for output"""
213
+ lines = []
214
+ lines.append("=== Operating System Detection ===")
215
+
216
+ if not os_data:
217
+ lines.append("OS detection unavailable")
218
+ lines.append("")
219
+ return '\n'.join(lines)
220
+
221
+ if os_data.get('has_raw_socket'):
222
+ lines.append("Method: Active fingerprinting (raw sockets available)")
223
+ else:
224
+ lines.append("Method: Passive analysis (no raw socket permissions)")
225
+ lines.append("Note: Run as root or grant CAP_NET_RAW for active OS fingerprinting")
226
+
227
+ lines.append("")
228
+
229
+ if os_data.get('ttl'):
230
+ lines.append(f"TTL: {os_data['ttl']}")
231
+
232
+ if os_data.get('os_guess'):
233
+ confidence = os_data.get('confidence', 'unknown')
234
+ lines.append(f"OS Guess: {os_data['os_guess']} (confidence: {confidence})")
235
+ else:
236
+ lines.append("OS: Unable to determine")
237
+
238
+ if not os_data.get('has_raw_socket'):
239
+ lines.append("")
240
+ lines.append("💡 Tip: For more accurate OS detection, run changedetection.io with:")
241
+ lines.append(" • Root privileges, or")
242
+ lines.append(" • CAP_NET_RAW capability: sudo setcap cap_net_raw+ep /path/to/python")
243
+
244
+ lines.append("")
245
+ return '\n'.join(lines)
@@ -0,0 +1,113 @@
1
+ """
2
+ Port Scanning Step
3
+ Fast asyncio-based TCP connect port scanning
4
+ """
5
+
6
+ import asyncio
7
+ # SOCKS5 proxy support: Port scanning not compatible with SOCKS5
8
+ supports_socks5 = False
9
+ from loguru import logger
10
+
11
+ # ============================================================================
12
+ # CONFIGURATION
13
+ # ============================================================================
14
+
15
+ # Port list extracted from /etc/services (TCP ports 1-10000)
16
+ # This hardcoded list works on all platforms (Windows, Linux, etc.)
17
+ COMMON_PORTS = [
18
+ 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 43, 49, 53, 70, 79, 80,
19
+ 88, 102, 104, 106, 110, 111, 113, 119, 135, 139, 143, 161, 162, 163, 164, 174,
20
+ 179, 199, 209, 210, 345, 346, 369, 370, 389, 427, 443, 444, 445, 464, 465, 487,
21
+ 512, 513, 514, 515, 538, 540, 543, 544, 548, 554, 563, 587, 607, 628, 631, 636,
22
+ 646, 655, 706, 749, 750, 751, 754, 775, 777, 783, 853, 871, 873, 989, 990, 992,
23
+ 993, 995, 1080, 1093, 1094, 1099, 1127, 1178, 1194, 1236, 1313, 1314, 1352, 1433,
24
+ 1524, 1645, 1646, 1649, 1677, 1812, 1813, 2000, 2049, 2086, 2101, 2119, 2121, 2135,
25
+ 2401, 2430, 2431, 2432, 2433, 2583, 2600, 2601
26
+ ]
27
+
28
+ # Service name mapping for common ports (extracted from /etc/services)
29
+ PORT_SERVICES = {
30
+ 1: 'tcpmux', 7: 'echo', 9: 'discard', 11: 'systat', 13: 'daytime', 15: 'netstat',
31
+ 17: 'qotd', 19: 'chargen', 20: 'ftp-data', 21: 'ftp', 22: 'ssh', 23: 'telnet',
32
+ 25: 'smtp', 37: 'time', 43: 'whois', 49: 'tacacs', 53: 'domain', 70: 'gopher',
33
+ 79: 'finger', 80: 'http', 88: 'kerberos', 102: 'iso-tsap', 104: 'acr-nema',
34
+ 106: 'poppassd', 110: 'pop3', 111: 'sunrpc', 113: 'auth', 119: 'nntp', 135: 'epmap',
35
+ 139: 'netbios-ssn', 143: 'imap2', 161: 'snmp', 162: 'snmp-trap', 163: 'cmip-man',
36
+ 164: 'cmip-agent', 174: 'mailq', 179: 'bgp', 199: 'smux', 209: 'qmtp', 210: 'z3950',
37
+ 345: 'pawserv', 346: 'zserv', 369: 'rpc2portmap', 370: 'codaauth2', 389: 'ldap',
38
+ 427: 'svrloc', 443: 'https', 444: 'snpp', 445: 'microsoft-ds', 464: 'kpasswd',
39
+ 465: 'submissions', 487: 'saft', 512: 'exec', 513: 'login', 514: 'shell', 515: 'printer',
40
+ 538: 'gdomap', 540: 'uucp', 543: 'klogin', 544: 'kshell', 548: 'afpovertcp', 554: 'rtsp',
41
+ 563: 'nntps', 587: 'submission', 607: 'nqs', 628: 'qmqp', 631: 'ipp', 636: 'ldaps',
42
+ 646: 'ldp', 655: 'tinc', 706: 'silc', 749: 'kerberos-adm', 750: 'kerberos4',
43
+ 751: 'kerberos-master', 754: 'krb-prop', 775: 'moira-db', 777: 'moira-update',
44
+ 783: 'spamd', 853: 'domain-s', 871: 'supfilesrv', 873: 'rsync', 989: 'ftps-data',
45
+ 990: 'ftps', 992: 'telnets', 993: 'imaps', 995: 'pop3s', 1080: 'socks', 1093: 'proofd',
46
+ 1094: 'rootd', 1099: 'rmiregistry', 1127: 'supfiledbg', 1178: 'skkserv', 1194: 'openvpn',
47
+ 1236: 'rmtcfg', 1313: 'xtel', 1314: 'xtelw', 1352: 'lotusnote', 1433: 'ms-sql-s',
48
+ 1524: 'ingreslock', 1645: 'datametrics', 1646: 'sa-msg-port', 1649: 'kermit',
49
+ 1677: 'groupwise', 1812: 'radius', 1813: 'radius-acct', 2000: 'cisco-sccp', 2049: 'nfs',
50
+ 2086: 'gnunet', 2101: 'rtcm-sc104', 2119: 'gsigatekeeper', 2121: 'iprop', 2135: 'gris',
51
+ 2401: 'cvspserver', 2430: 'venus', 2431: 'venus-se', 2432: 'codasrv', 2433: 'codasrv-se',
52
+ 2583: 'mon', 2600: 'zebrasrv', 2601: 'zebra',
53
+ 3306: 'mysql', 3389: 'ms-wbt-server', 5432: 'postgresql', 5900: 'vnc',
54
+ 8080: 'http-alt', 8443: 'https-alt'
55
+ }
56
+
57
+
58
+ async def scan_ports(ip_address, ports=None, watch_uuid=None, update_signal=None):
59
+ """
60
+ Perform fast TCP connect port scan
61
+
62
+ Args:
63
+ ip_address: Target IP address
64
+ ports: List of ports to scan (default: common service ports)
65
+ watch_uuid: Optional watch UUID for status updates
66
+ update_signal: Optional blinker signal for status updates
67
+
68
+ Returns:
69
+ list: List of open ports
70
+ """
71
+ if update_signal and watch_uuid:
72
+ update_signal.send(watch_uuid=watch_uuid, status="Ports")
73
+
74
+ if ports is None:
75
+ # Use ports from /etc/services
76
+ ports = COMMON_PORTS
77
+
78
+ async def check_port(host, port, timeout=0.5):
79
+ """Fast TCP connect scan for a single port"""
80
+ try:
81
+ reader, writer = await asyncio.wait_for(
82
+ asyncio.open_connection(host, port),
83
+ timeout=timeout
84
+ )
85
+ writer.close()
86
+ await writer.wait_closed()
87
+ return port
88
+ except:
89
+ return None
90
+
91
+ # Scan all ports in parallel
92
+ scan_tasks = [check_port(ip_address, port) for port in ports]
93
+ scan_results = await asyncio.gather(*scan_tasks)
94
+ open_ports = sorted([p for p in scan_results if p is not None])
95
+
96
+ return open_ports
97
+
98
+
99
+ def format_portscan_results(open_ports):
100
+ """Format port scan results for output"""
101
+ lines = []
102
+ lines.append("=== Port Scan ===")
103
+
104
+ if open_ports:
105
+ lines.append(f"Open Ports ({len(open_ports)} found):")
106
+ for port in open_ports:
107
+ service = PORT_SERVICES.get(port, 'unknown')
108
+ lines.append(f" {port}: {service}")
109
+ else:
110
+ lines.append("No open ports found (or filtered)")
111
+
112
+ lines.append("")
113
+ return '\n'.join(lines)
@@ -0,0 +1,49 @@
1
+ """
2
+ Step Registry - Automatically discovers and registers all scan steps
3
+ """
4
+
5
+ from typing import List, Type
6
+ from .base import ScanStep
7
+ from loguru import logger
8
+
9
+
10
+ # Registry of all scan step classes
11
+ _STEPS: List[Type[ScanStep]] = []
12
+
13
+
14
+ def register_step(step_class: Type[ScanStep]):
15
+ """
16
+ Register a scan step class.
17
+
18
+ Args:
19
+ step_class: ScanStep subclass to register
20
+ """
21
+ _STEPS.append(step_class)
22
+ logger.debug(f"Registered OSINT scan step: {step_class.name} (order: {step_class.order})")
23
+ return step_class
24
+
25
+
26
+ def get_all_steps() -> List[Type[ScanStep]]:
27
+ """
28
+ Get all registered scan steps, sorted by order.
29
+
30
+ Returns:
31
+ List of ScanStep classes sorted by their order attribute
32
+ """
33
+ return sorted(_STEPS, key=lambda s: s.order)
34
+
35
+
36
+ def discover_steps():
37
+ """
38
+ Auto-discover and import all step modules.
39
+
40
+ This function imports all step modules which triggers their @register_step decorators.
41
+ """
42
+ # Import all step modules to trigger registration
43
+ from . import dns_scan
44
+ from . import whois_scan
45
+ from . import http_scan
46
+ from . import tls_scan
47
+ from . import port_scan
48
+ from . import traceroute_scan
49
+ from . import bgp_scan