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,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Traceroute Step
|
|
3
|
+
Performs traceroute and displays last N hops to target
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
# SOCKS5 proxy support: Traceroute uses ICMP/UDP, incompatible with SOCKS5
|
|
8
|
+
supports_socks5 = False
|
|
9
|
+
import socket
|
|
10
|
+
import struct
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
# Configuration: Number of last hops to display
|
|
14
|
+
TRACEROUTE_LAST_HOPS = 3
|
|
15
|
+
|
|
16
|
+
# Maximum TTL to try
|
|
17
|
+
MAX_TTL = 30
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def scan_traceroute(ip_address, dns_resolver, last_n_hops=TRACEROUTE_LAST_HOPS, watch_uuid=None, update_signal=None):
|
|
21
|
+
"""
|
|
22
|
+
Perform traceroute and return last N hops
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
ip_address: Target IP address
|
|
26
|
+
dns_resolver: DNS resolver for reverse lookups
|
|
27
|
+
last_n_hops: Number of last hops to return (default: 3)
|
|
28
|
+
watch_uuid: Optional watch UUID for status updates
|
|
29
|
+
update_signal: Optional blinker signal for status updates
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
list: List of last N hops with hop number, IP, and hostname
|
|
33
|
+
"""
|
|
34
|
+
if update_signal and watch_uuid:
|
|
35
|
+
update_signal.send(watch_uuid=watch_uuid, status="Trace")
|
|
36
|
+
|
|
37
|
+
def run_traceroute():
|
|
38
|
+
"""Perform traceroute using ICMP (requires raw sockets) or UDP fallback"""
|
|
39
|
+
import subprocess
|
|
40
|
+
import re
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Use system traceroute command (works without root for UDP)
|
|
44
|
+
# -n: no DNS resolution (we'll do it ourselves)
|
|
45
|
+
# -m: max hops
|
|
46
|
+
# -w: timeout per hop
|
|
47
|
+
# -q: queries per hop
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
['traceroute', '-n', '-m', str(MAX_TTL), '-w', '1', '-q', '1', ip_address],
|
|
50
|
+
capture_output=True,
|
|
51
|
+
text=True,
|
|
52
|
+
timeout=15
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if result.returncode != 0:
|
|
56
|
+
logger.warning(f"Traceroute command failed: {result.stderr}")
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
# Parse traceroute output
|
|
60
|
+
hops = []
|
|
61
|
+
lines = result.stdout.strip().split('\n')
|
|
62
|
+
|
|
63
|
+
for line in lines[1:]: # Skip header line
|
|
64
|
+
# Parse line like: " 1 192.168.1.1 1.234 ms"
|
|
65
|
+
match = re.search(r'^\s*(\d+)\s+([\d\.]+|[\da-f:]+)\s+', line)
|
|
66
|
+
if match:
|
|
67
|
+
hop_num = int(match.group(1))
|
|
68
|
+
hop_ip = match.group(2)
|
|
69
|
+
|
|
70
|
+
# Skip if it's a timeout (* * *)
|
|
71
|
+
if '*' in line and hop_ip not in line:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Reverse DNS lookup for this hop
|
|
75
|
+
try:
|
|
76
|
+
import dns.reversename
|
|
77
|
+
rev_name = dns.reversename.from_address(hop_ip)
|
|
78
|
+
answers = dns_resolver.resolve(rev_name, 'PTR')
|
|
79
|
+
hostname = str(answers[0])
|
|
80
|
+
except:
|
|
81
|
+
hostname = ""
|
|
82
|
+
|
|
83
|
+
hops.append({
|
|
84
|
+
'hop': hop_num,
|
|
85
|
+
'ip': hop_ip,
|
|
86
|
+
'hostname': hostname
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
# Return only last N hops
|
|
90
|
+
if hops:
|
|
91
|
+
return hops[-last_n_hops:]
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
except subprocess.TimeoutExpired:
|
|
95
|
+
logger.warning("Traceroute timed out")
|
|
96
|
+
return []
|
|
97
|
+
except FileNotFoundError:
|
|
98
|
+
logger.warning("traceroute command not found, skipping traceroute")
|
|
99
|
+
return []
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.error(f"Traceroute failed: {e}")
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
return await asyncio.to_thread(run_traceroute)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Traceroute scan failed: {e}")
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def format_traceroute_results(hops):
|
|
112
|
+
"""Format traceroute results for output"""
|
|
113
|
+
lines = []
|
|
114
|
+
lines.append(f"=== Traceroute (Last {TRACEROUTE_LAST_HOPS} Hops) ===")
|
|
115
|
+
|
|
116
|
+
if hops:
|
|
117
|
+
for hop in hops:
|
|
118
|
+
if hop['hostname']:
|
|
119
|
+
lines.append(f" {hop['hop']:2d}. {hop['ip']:15s} ({hop['hostname']})")
|
|
120
|
+
else:
|
|
121
|
+
lines.append(f" {hop['hop']:2d}. {hop['ip']:15s}")
|
|
122
|
+
else:
|
|
123
|
+
lines.append("Traceroute data not available")
|
|
124
|
+
lines.append("(requires traceroute command or may be blocked by firewall)")
|
|
125
|
+
|
|
126
|
+
lines.append("")
|
|
127
|
+
return '\n'.join(lines)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WHOIS Lookup Step
|
|
3
|
+
Performs WHOIS queries using python-whois library
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
# SOCKS5 proxy support: python-whois library doesn't support SOCKS5 (TODO: implement raw WHOIS over SOCKS5)
|
|
8
|
+
supports_socks5 = False
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def scan_whois(hostname, watch_uuid=None, update_signal=None):
|
|
14
|
+
"""
|
|
15
|
+
Perform WHOIS lookup on hostname
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
hostname: Target hostname
|
|
19
|
+
watch_uuid: Optional watch UUID for status updates
|
|
20
|
+
update_signal: Optional blinker signal for status updates
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
whois.WhoisEntry or None: WHOIS data
|
|
24
|
+
"""
|
|
25
|
+
if update_signal and watch_uuid:
|
|
26
|
+
update_signal.send(watch_uuid=watch_uuid, status="WHOIS")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import whois
|
|
30
|
+
return await asyncio.to_thread(whois.whois, hostname)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"WHOIS lookup failed: {e}")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def format_whois_results(whois_data, expire_warning_days=3):
|
|
37
|
+
"""Format WHOIS results for output
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
whois_data: WHOIS data object
|
|
41
|
+
expire_warning_days: Number of days before expiration to show warning (default: 3)
|
|
42
|
+
"""
|
|
43
|
+
lines = []
|
|
44
|
+
lines.append("=== WHOIS Information ===")
|
|
45
|
+
|
|
46
|
+
if whois_data:
|
|
47
|
+
field_map = {
|
|
48
|
+
'domain_name': 'Domain Name',
|
|
49
|
+
'registrar': 'Registrar',
|
|
50
|
+
'whois_server': 'WHOIS Server',
|
|
51
|
+
'creation_date': 'Creation Date',
|
|
52
|
+
'expiration_date': 'Expiration Date',
|
|
53
|
+
'updated_date': 'Updated Date',
|
|
54
|
+
'name_servers': 'Name Servers',
|
|
55
|
+
'status': 'Status',
|
|
56
|
+
'dnssec': 'DNSSEC',
|
|
57
|
+
'org': 'Organization',
|
|
58
|
+
'country': 'Country',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
expiration_date = None
|
|
62
|
+
for key, label in field_map.items():
|
|
63
|
+
value = getattr(whois_data, key, None)
|
|
64
|
+
if value:
|
|
65
|
+
if isinstance(value, list):
|
|
66
|
+
# Take first item for dates, show all for name servers
|
|
67
|
+
if key in ['creation_date', 'expiration_date', 'updated_date']:
|
|
68
|
+
value = value[0] if value else None
|
|
69
|
+
if value:
|
|
70
|
+
lines.append(f"{label}: {value}")
|
|
71
|
+
# Track expiration date for countdown calculation
|
|
72
|
+
if key == 'expiration_date':
|
|
73
|
+
expiration_date = value
|
|
74
|
+
else:
|
|
75
|
+
lines.append(f"{label}:")
|
|
76
|
+
for item in value[:10]: # Limit to 10 items
|
|
77
|
+
lines.append(f" - {item}")
|
|
78
|
+
else:
|
|
79
|
+
lines.append(f"{label}: {value}")
|
|
80
|
+
# Track expiration date for countdown calculation
|
|
81
|
+
if key == 'expiration_date':
|
|
82
|
+
expiration_date = value
|
|
83
|
+
|
|
84
|
+
# Add expiration countdown if within configured warning days
|
|
85
|
+
if expiration_date and expire_warning_days > 0:
|
|
86
|
+
try:
|
|
87
|
+
# Ensure expiration_date is a datetime object
|
|
88
|
+
if not isinstance(expiration_date, datetime):
|
|
89
|
+
date_str = str(expiration_date)
|
|
90
|
+
# Try multiple parsing strategies
|
|
91
|
+
try:
|
|
92
|
+
# Try ISO format with Z replacement
|
|
93
|
+
expiration_date = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
94
|
+
except (ValueError, AttributeError):
|
|
95
|
+
# Try parsing without timezone
|
|
96
|
+
from dateutil import parser
|
|
97
|
+
expiration_date = parser.parse(date_str)
|
|
98
|
+
|
|
99
|
+
# Make timezone-aware if needed
|
|
100
|
+
if expiration_date.tzinfo is None:
|
|
101
|
+
expiration_date = expiration_date.replace(tzinfo=timezone.utc)
|
|
102
|
+
# Convert to UTC if it's in a different timezone
|
|
103
|
+
elif expiration_date.tzinfo != timezone.utc:
|
|
104
|
+
expiration_date = expiration_date.astimezone(timezone.utc)
|
|
105
|
+
|
|
106
|
+
now = datetime.now(timezone.utc)
|
|
107
|
+
days_to_expire = (expiration_date - now).days
|
|
108
|
+
|
|
109
|
+
if days_to_expire <= expire_warning_days and days_to_expire >= 0:
|
|
110
|
+
if days_to_expire == 0:
|
|
111
|
+
lines.append("⚠️ WARNING: Domain expires TODAY!")
|
|
112
|
+
elif days_to_expire == 1:
|
|
113
|
+
lines.append("⚠️ WARNING: Domain expires in 1 day")
|
|
114
|
+
else:
|
|
115
|
+
lines.append(f"⚠️ WARNING: Domain expires in {days_to_expire} days")
|
|
116
|
+
elif days_to_expire < 0:
|
|
117
|
+
lines.append("⚠️ WARNING: Domain has EXPIRED!")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Could not parse expiration date for countdown: {e}")
|
|
120
|
+
lines.append(f"⚠️ ERROR: Could not parse expiration date format: {e}")
|
|
121
|
+
else:
|
|
122
|
+
lines.append("No WHOIS data available")
|
|
123
|
+
|
|
124
|
+
lines.append("")
|
|
125
|
+
return '\n'.join(lines)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WHOIS Lookup Step
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from .base import ScanStep
|
|
9
|
+
from .registry import register_step
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@register_step
|
|
13
|
+
class WHOISScanStep(ScanStep):
|
|
14
|
+
"""WHOIS domain registration information"""
|
|
15
|
+
|
|
16
|
+
name = "WHOIS Information"
|
|
17
|
+
order = 20
|
|
18
|
+
|
|
19
|
+
async def scan(self, context: dict):
|
|
20
|
+
"""Perform WHOIS lookup"""
|
|
21
|
+
hostname = context['hostname']
|
|
22
|
+
watch_uuid = context.get('watch_uuid')
|
|
23
|
+
update_signal = context.get('update_signal')
|
|
24
|
+
|
|
25
|
+
if update_signal and watch_uuid:
|
|
26
|
+
update_signal.send(watch_uuid=watch_uuid, status="WHOIS")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import whois
|
|
30
|
+
return await asyncio.to_thread(whois.whois, hostname)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"WHOIS lookup failed: {e}")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
def format_results(self, whois_data, expire_warning_days=3):
|
|
36
|
+
"""Format WHOIS results
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
whois_data: WHOIS data object
|
|
40
|
+
expire_warning_days: Number of days before expiration to show warning (default: 3)
|
|
41
|
+
"""
|
|
42
|
+
lines = []
|
|
43
|
+
lines.append("=== WHOIS Information ===")
|
|
44
|
+
|
|
45
|
+
if whois_data and not isinstance(whois_data, Exception):
|
|
46
|
+
field_map = {
|
|
47
|
+
'domain_name': 'Domain Name',
|
|
48
|
+
'registrar': 'Registrar',
|
|
49
|
+
'whois_server': 'WHOIS Server',
|
|
50
|
+
'creation_date': 'Creation Date',
|
|
51
|
+
'expiration_date': 'Expiration Date',
|
|
52
|
+
'updated_date': 'Updated Date',
|
|
53
|
+
'name_servers': 'Name Servers',
|
|
54
|
+
'status': 'Status',
|
|
55
|
+
'dnssec': 'DNSSEC',
|
|
56
|
+
'org': 'Organization',
|
|
57
|
+
'country': 'Country',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
expiration_date = None
|
|
61
|
+
for key, label in field_map.items():
|
|
62
|
+
value = getattr(whois_data, key, None)
|
|
63
|
+
if value:
|
|
64
|
+
if isinstance(value, list):
|
|
65
|
+
if key in ['creation_date', 'expiration_date', 'updated_date']:
|
|
66
|
+
value = value[0] if value else None
|
|
67
|
+
if value:
|
|
68
|
+
lines.append(f"{label}: {value}")
|
|
69
|
+
# Track expiration date for countdown calculation
|
|
70
|
+
if key == 'expiration_date':
|
|
71
|
+
expiration_date = value
|
|
72
|
+
else:
|
|
73
|
+
lines.append(f"{label}:")
|
|
74
|
+
for item in value[:10]:
|
|
75
|
+
lines.append(f" - {item}")
|
|
76
|
+
else:
|
|
77
|
+
lines.append(f"{label}: {value}")
|
|
78
|
+
# Track expiration date for countdown calculation
|
|
79
|
+
if key == 'expiration_date':
|
|
80
|
+
expiration_date = value
|
|
81
|
+
|
|
82
|
+
# Add expiration countdown if within configured warning days
|
|
83
|
+
if expiration_date and expire_warning_days > 0:
|
|
84
|
+
try:
|
|
85
|
+
# Ensure expiration_date is a datetime object
|
|
86
|
+
if not isinstance(expiration_date, datetime):
|
|
87
|
+
date_str = str(expiration_date)
|
|
88
|
+
# Try multiple parsing strategies
|
|
89
|
+
try:
|
|
90
|
+
# Try ISO format with Z replacement
|
|
91
|
+
expiration_date = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
|
92
|
+
except (ValueError, AttributeError):
|
|
93
|
+
# Try parsing without timezone
|
|
94
|
+
from dateutil import parser
|
|
95
|
+
expiration_date = parser.parse(date_str)
|
|
96
|
+
|
|
97
|
+
# Make timezone-aware if needed
|
|
98
|
+
if expiration_date.tzinfo is None:
|
|
99
|
+
expiration_date = expiration_date.replace(tzinfo=timezone.utc)
|
|
100
|
+
# Convert to UTC if it's in a different timezone
|
|
101
|
+
elif expiration_date.tzinfo != timezone.utc:
|
|
102
|
+
expiration_date = expiration_date.astimezone(timezone.utc)
|
|
103
|
+
|
|
104
|
+
now = datetime.now(timezone.utc)
|
|
105
|
+
days_to_expire = (expiration_date - now).days
|
|
106
|
+
|
|
107
|
+
if days_to_expire <= expire_warning_days and days_to_expire >= 0:
|
|
108
|
+
if days_to_expire == 0:
|
|
109
|
+
lines.append("⚠️ WARNING: Domain expires TODAY!")
|
|
110
|
+
elif days_to_expire == 1:
|
|
111
|
+
lines.append("⚠️ WARNING: Domain expires in 1 day")
|
|
112
|
+
else:
|
|
113
|
+
lines.append(f"⚠️ WARNING: Domain expires in {days_to_expire} days")
|
|
114
|
+
elif days_to_expire < 0:
|
|
115
|
+
lines.append("⚠️ WARNING: Domain has EXPIRED!")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Could not parse expiration date for countdown: {e}")
|
|
118
|
+
lines.append(f"⚠️ ERROR: Could not parse expiration date format: {e}")
|
|
119
|
+
else:
|
|
120
|
+
lines.append("No WHOIS data available")
|
|
121
|
+
|
|
122
|
+
lines.append("")
|
|
123
|
+
return '\n'.join(lines)
|