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.
- changedetection_io_osint_processor-0.0.1.dist-info/METADATA +274 -0
- changedetection_io_osint_processor-0.0.1.dist-info/RECORD +29 -0
- changedetection_io_osint_processor-0.0.1.dist-info/WHEEL +5 -0
- changedetection_io_osint_processor-0.0.1.dist-info/entry_points.txt +2 -0
- changedetection_io_osint_processor-0.0.1.dist-info/licenses/LICENSE +661 -0
- changedetection_io_osint_processor-0.0.1.dist-info/top_level.txt +1 -0
- changedetectionio_osint/__init__.py +22 -0
- changedetectionio_osint/forms.py +289 -0
- changedetectionio_osint/plugin.py +37 -0
- changedetectionio_osint/processor.py +655 -0
- changedetectionio_osint/steps/__init__.py +4 -0
- changedetectionio_osint/steps/base.py +76 -0
- changedetectionio_osint/steps/bgp.py +88 -0
- changedetectionio_osint/steps/dns.py +147 -0
- changedetectionio_osint/steps/dns_scan.py +88 -0
- changedetectionio_osint/steps/dnssec.py +260 -0
- changedetectionio_osint/steps/email_security.py +236 -0
- changedetectionio_osint/steps/http_fingerprint.py +359 -0
- changedetectionio_osint/steps/http_scan.py +31 -0
- changedetectionio_osint/steps/mac_lookup.py +209 -0
- changedetectionio_osint/steps/os_detection.py +245 -0
- changedetectionio_osint/steps/portscan.py +113 -0
- changedetectionio_osint/steps/registry.py +49 -0
- changedetectionio_osint/steps/smtp_fingerprint.py +517 -0
- changedetectionio_osint/steps/ssh_fingerprint.py +310 -0
- changedetectionio_osint/steps/tls_analysis.py +332 -0
- changedetectionio_osint/steps/traceroute.py +127 -0
- changedetectionio_osint/steps/whois_lookup.py +125 -0
- 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
|