scopez 0.1.8__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.
- __init__.py +0 -0
- __version__.py +1 -0
- analysis.py +390 -0
- icons/404.png +0 -0
- icons/cidr.png +0 -0
- icons/fqdn.png +0 -0
- icons/ipv4.png +0 -0
- icons/target.png +0 -0
- icons/url.png +0 -0
- main.py +305 -0
- models.py +57 -0
- print.py +236 -0
- scopez-0.1.8.dist-info/METADATA +170 -0
- scopez-0.1.8.dist-info/RECORD +23 -0
- scopez-0.1.8.dist-info/WHEEL +5 -0
- scopez-0.1.8.dist-info/entry_points.txt +2 -0
- scopez-0.1.8.dist-info/licenses/LICENSE +21 -0
- scopez-0.1.8.dist-info/top_level.txt +12 -0
- targets.py +304 -0
- utils.py +44 -0
- validation.py +44 -0
- verbose.py +50 -0
- visualization.py +148 -0
__init__.py
ADDED
|
File without changes
|
__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = 'v0.1.8'
|
analysis.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import pydantic
|
|
2
|
+
import ipwhois
|
|
3
|
+
import dns.resolver
|
|
4
|
+
import requests
|
|
5
|
+
import pandas
|
|
6
|
+
|
|
7
|
+
import ipaddress
|
|
8
|
+
import subprocess
|
|
9
|
+
import os
|
|
10
|
+
import concurrent.futures
|
|
11
|
+
import random
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
import models
|
|
16
|
+
import verbose
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Analyzer(pydantic.BaseModel):
|
|
20
|
+
"""Analyzes raw targets and populates them with additiona information."""
|
|
21
|
+
|
|
22
|
+
analyzed_ipv4s: list[models.IPV4] = []
|
|
23
|
+
analyzed_cidrs: list[models.CIDR] = []
|
|
24
|
+
analyzed_fqdns: list[models.FQDN] = []
|
|
25
|
+
analyzed_urls: list[models.URL] = []
|
|
26
|
+
geoip_records: list[models.GeoIPRecord] = []
|
|
27
|
+
|
|
28
|
+
def parse_geoip_data(self, geoip_csv_database_filepath: str):
|
|
29
|
+
data_frame = pandas.read_csv(geoip_csv_database_filepath).fillna('')
|
|
30
|
+
records = data_frame.to_dict(orient='records')
|
|
31
|
+
|
|
32
|
+
for r in records:
|
|
33
|
+
self.geoip_records.append(
|
|
34
|
+
models.GeoIPRecord(
|
|
35
|
+
network=r.get('network'),
|
|
36
|
+
geoname_id=r.get('geoname_id'),
|
|
37
|
+
continent_code=r.get('continent_code'),
|
|
38
|
+
continent_name=r.get('continent_name'),
|
|
39
|
+
country_iso_code=r.get('country_iso_code'),
|
|
40
|
+
country_name=r.get('country_name'),
|
|
41
|
+
is_anonymous_proxy=r.get('is_anonymous_proxy'),
|
|
42
|
+
is_satellite_provider=r.get('is_satellite_provider'),
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def analyze_ipv4s(self, ipv4s: list[str], no_threads: int) -> None:
|
|
47
|
+
"""Analyze the IP addresses (v4) and populate them with information.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
ipv4s (list[str]): A list of IP addresses in raw format.
|
|
51
|
+
no_threads(int): The number of worker threads to run in-parallel.
|
|
52
|
+
"""
|
|
53
|
+
verbose.debug('Analyze IPV4s.')
|
|
54
|
+
|
|
55
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=no_threads) as executor:
|
|
56
|
+
futures = [executor.submit(self._populate_ipv4, ip) for ip in ipv4s]
|
|
57
|
+
|
|
58
|
+
for future in concurrent.futures.as_completed(futures):
|
|
59
|
+
ipv4_obj = future.result()
|
|
60
|
+
verbose.normal(ipv4_obj.ipv4)
|
|
61
|
+
self.analyzed_ipv4s.append(ipv4_obj)
|
|
62
|
+
|
|
63
|
+
def analyze_cidrs(self, cidrs: list[str], no_threads: int) -> None:
|
|
64
|
+
"""Analyze the CIDR IP addresses (v4) and populate them with information.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
cidrs (list[str]): A list of CIDR IP addresses in raw format.
|
|
68
|
+
no_threads(int): The number of worker threads to run in-parallel.
|
|
69
|
+
"""
|
|
70
|
+
verbose.debug('Analyze CIDRs.')
|
|
71
|
+
|
|
72
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=no_threads) as executor:
|
|
73
|
+
futures = [executor.submit(self._populate_cidr, cidr) for cidr in cidrs]
|
|
74
|
+
|
|
75
|
+
for future in concurrent.futures.as_completed(futures):
|
|
76
|
+
cidr_obj = future.result()
|
|
77
|
+
verbose.normal(cidr_obj.cidr)
|
|
78
|
+
self.analyzed_cidrs.append(cidr_obj)
|
|
79
|
+
|
|
80
|
+
def analyze_fqdns(self, fqdns: list[str], no_threads: int) -> None:
|
|
81
|
+
"""Analyze the FQDNs and populate them with information.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
fqdns (list[str]): A list of FQDNs in raw format.
|
|
85
|
+
no_threads(int): The number of worker threads to run in-parallel.
|
|
86
|
+
"""
|
|
87
|
+
verbose.debug('Analyze FQDNs.')
|
|
88
|
+
|
|
89
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=no_threads) as executor:
|
|
90
|
+
futures = [executor.submit(self._populate_fqdn, fqdn) for fqdn in fqdns]
|
|
91
|
+
|
|
92
|
+
for future in concurrent.futures.as_completed(futures):
|
|
93
|
+
fqdn_obj = future.result()
|
|
94
|
+
verbose.normal(fqdn_obj.fqdn)
|
|
95
|
+
self.analyzed_fqdns.append(fqdn_obj)
|
|
96
|
+
|
|
97
|
+
def analyze_urls(self, urls: list[str], no_threads: int) -> None:
|
|
98
|
+
"""Analyze the URLS and populate them with information.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
urls (list[str]): A list of URLs in raw format.
|
|
102
|
+
no_threads(int): The number of worker threads to run in-parallel.
|
|
103
|
+
"""
|
|
104
|
+
verbose.debug('Analyze URLs.')
|
|
105
|
+
|
|
106
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=no_threads) as executor:
|
|
107
|
+
futures = [executor.submit(self._populate_url, url) for url in urls]
|
|
108
|
+
|
|
109
|
+
for future in concurrent.futures.as_completed(futures):
|
|
110
|
+
url_obj = future.result()
|
|
111
|
+
verbose.normal(url_obj.url)
|
|
112
|
+
self.analyzed_urls.append(url_obj)
|
|
113
|
+
|
|
114
|
+
def _populate_ipv4(self, ipv4: str) -> models.IPV4:
|
|
115
|
+
verbose.debug(f'Analyze {ipv4}.')
|
|
116
|
+
|
|
117
|
+
ipv4_obj = models.IPV4()
|
|
118
|
+
ipv4_obj.ipv4 = ipv4
|
|
119
|
+
|
|
120
|
+
########
|
|
121
|
+
# Math #
|
|
122
|
+
########
|
|
123
|
+
ip = ipaddress.IPv4Address(ipv4)
|
|
124
|
+
|
|
125
|
+
##############
|
|
126
|
+
# Visibility #
|
|
127
|
+
##############
|
|
128
|
+
ipv4_obj.visibility = 'Private' if ip.is_private else 'Public'
|
|
129
|
+
|
|
130
|
+
########
|
|
131
|
+
# RDAP #
|
|
132
|
+
########
|
|
133
|
+
if ipv4_obj.visibility == 'Public':
|
|
134
|
+
while True:
|
|
135
|
+
try:
|
|
136
|
+
whois = ipwhois.IPWhois(ipv4_obj.ipv4)
|
|
137
|
+
rdap = whois.lookup_rdap(depth=1) or {}
|
|
138
|
+
break
|
|
139
|
+
except Exception:
|
|
140
|
+
time.sleep(5)
|
|
141
|
+
continue
|
|
142
|
+
ipv4_obj.asn_network = rdap.get('network', {}).get('name', '').replace(',', '')
|
|
143
|
+
ipv4_obj.asn_country_code = rdap.get('asn_country_code')
|
|
144
|
+
ipv4_obj.asn_description = rdap.get('asn_description', '').replace(',', '')
|
|
145
|
+
else:
|
|
146
|
+
ipv4_obj.asn_network = 'N/A'
|
|
147
|
+
ipv4_obj.asn_country_code = 'N/A'
|
|
148
|
+
ipv4_obj.asn_description = 'N/A'
|
|
149
|
+
|
|
150
|
+
#########
|
|
151
|
+
# GeoIP #
|
|
152
|
+
#########
|
|
153
|
+
if ipv4_obj.visibility == 'Public':
|
|
154
|
+
for r in self.geoip_records:
|
|
155
|
+
network = ipaddress.IPv4Network(r.network)
|
|
156
|
+
|
|
157
|
+
if ip in network:
|
|
158
|
+
ipv4_obj.geoip_continent = r.continent_name
|
|
159
|
+
ipv4_obj.geoip_country = r.country_name
|
|
160
|
+
else:
|
|
161
|
+
ipv4_obj.geoip_continent = 'N/A'
|
|
162
|
+
ipv4_obj.geoip_country = 'N/A'
|
|
163
|
+
|
|
164
|
+
########
|
|
165
|
+
# Ping #
|
|
166
|
+
########
|
|
167
|
+
if ipv4_obj.visibility == 'Public':
|
|
168
|
+
param = '-n' if os.sys.platform.lower() == 'win32' else '-c'
|
|
169
|
+
command = ['ping', param, '1', '-i', '0.2', ipv4]
|
|
170
|
+
pingable = subprocess.call(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
|
|
171
|
+
ipv4_obj.pingable = pingable
|
|
172
|
+
else:
|
|
173
|
+
ipv4_obj.pingable = False
|
|
174
|
+
|
|
175
|
+
return ipv4_obj
|
|
176
|
+
|
|
177
|
+
def _populate_cidr(self, cidr: str) -> models.CIDR:
|
|
178
|
+
verbose.debug(f'Analyze {cidr}.')
|
|
179
|
+
|
|
180
|
+
cidr_obj = models.CIDR()
|
|
181
|
+
cidr_obj.cidr = cidr
|
|
182
|
+
|
|
183
|
+
########
|
|
184
|
+
# Math #
|
|
185
|
+
########
|
|
186
|
+
network = ipaddress.IPv4Network(cidr, strict=False)
|
|
187
|
+
cidr_obj.number_of_hosts = network.num_addresses - 2
|
|
188
|
+
|
|
189
|
+
##############
|
|
190
|
+
# Visibility #
|
|
191
|
+
##############
|
|
192
|
+
cidr_obj.visibility = 'Private' if network.is_private else 'Public'
|
|
193
|
+
|
|
194
|
+
########
|
|
195
|
+
# RDAP #
|
|
196
|
+
########
|
|
197
|
+
if cidr_obj.visibility == 'Public':
|
|
198
|
+
ip = cidr.split('/')[0]
|
|
199
|
+
while True:
|
|
200
|
+
try:
|
|
201
|
+
whois = ipwhois.IPWhois(ip)
|
|
202
|
+
rdap = whois.lookup_rdap(depth=1) or {}
|
|
203
|
+
break
|
|
204
|
+
except Exception:
|
|
205
|
+
time.sleep(5)
|
|
206
|
+
continue
|
|
207
|
+
cidr_obj.asn_network = rdap.get('network', {}).get('name', '').replace(',', '')
|
|
208
|
+
cidr_obj.asn_country_code = rdap.get('asn_country_code')
|
|
209
|
+
cidr_obj.asn_description = rdap.get('asn_description', '').replace(',', '')
|
|
210
|
+
else:
|
|
211
|
+
cidr_obj.asn_network = 'N/A'
|
|
212
|
+
cidr_obj.asn_country_code = 'N/A'
|
|
213
|
+
cidr_obj.asn_description = 'N/A'
|
|
214
|
+
|
|
215
|
+
#########
|
|
216
|
+
# GeoIP #
|
|
217
|
+
#########
|
|
218
|
+
if cidr_obj.visibility == 'Public':
|
|
219
|
+
for r in self.geoip_records:
|
|
220
|
+
if cidr_obj.asn_network == r.network:
|
|
221
|
+
cidr_obj.geoip_continent = r.continent_name
|
|
222
|
+
cidr_obj.geoip_country = r.country_name
|
|
223
|
+
else:
|
|
224
|
+
cidr_obj.geoip_continent = 'N/A'
|
|
225
|
+
cidr_obj.geoip_country = 'N/A'
|
|
226
|
+
|
|
227
|
+
return cidr_obj
|
|
228
|
+
|
|
229
|
+
def _populate_fqdn(self, fqdn: str) -> models.FQDN:
|
|
230
|
+
verbose.debug(f'Analyze {fqdn}.')
|
|
231
|
+
|
|
232
|
+
######################################################################################
|
|
233
|
+
# Process #
|
|
234
|
+
# --- #
|
|
235
|
+
# Notes: #
|
|
236
|
+
# - We need to use different DNS every time otherwise we trigger a DOS response. #
|
|
237
|
+
# - A FQDN can point only to a *single* CNAME (RFC 1034 & RFC 2181). #
|
|
238
|
+
# - A FQDN can point to multiple IP addresses (Load-Balancing). #
|
|
239
|
+
# - A FQDN cannot have a CNAME and A/AAA records at the same time. #
|
|
240
|
+
# Code: #
|
|
241
|
+
# - Keep a list of FQDNs to resolve. #
|
|
242
|
+
# - Ask DNS server for CNAME records of FQDN. #
|
|
243
|
+
# - If there is a CNAME record, append it in the list of FQDNs to resolve. #
|
|
244
|
+
# - else check for A records for the hostname. #
|
|
245
|
+
######################################################################################
|
|
246
|
+
f = models.FQDN(fqdn=fqdn, dns_chain=[fqdn], destination_ips=[])
|
|
247
|
+
|
|
248
|
+
#############################
|
|
249
|
+
# Discover the CNAME Chain. #
|
|
250
|
+
#############################
|
|
251
|
+
while True:
|
|
252
|
+
cname_record = ''
|
|
253
|
+
try:
|
|
254
|
+
answer = dns.resolver.resolve_at(random.choice(DNS_SERVERS), f.dns_chain[-1], 'CNAME')
|
|
255
|
+
time.sleep(1)
|
|
256
|
+
|
|
257
|
+
for rdap in answer:
|
|
258
|
+
cname_record = str(rdap.target).rstrip('.') # Remove the trailing dot.
|
|
259
|
+
|
|
260
|
+
except dns.resolver.NXDOMAIN:
|
|
261
|
+
# NXDOMAIN stands for Non-Existent Domain.
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
except dns.resolver.NoAnswer:
|
|
265
|
+
# The domain does exist, but the specific DNS record type you're asking for is missing.
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
except dns.resolver.LifetimeTimeout:
|
|
269
|
+
# The resolution lifetime expired.
|
|
270
|
+
verbose.debug('dns.resolver.LifetimeTimeout')
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
except dns.resolver.NoNameservers:
|
|
274
|
+
# If no non-broken nameservers are available to answer the question.
|
|
275
|
+
verbose.debug('dns.resolver.NoNameservers')
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
f.dns_chain.append(cname_record)
|
|
279
|
+
|
|
280
|
+
############################################################
|
|
281
|
+
# For the last link in the DNS chain, check its A records. #
|
|
282
|
+
############################################################
|
|
283
|
+
while True:
|
|
284
|
+
try:
|
|
285
|
+
answer = dns.resolver.resolve_at(random.choice(DNS_SERVERS), f.dns_chain[-1], 'A')
|
|
286
|
+
time.sleep(1)
|
|
287
|
+
|
|
288
|
+
resolved_ips = []
|
|
289
|
+
for rdap in answer:
|
|
290
|
+
resolved_ips.append(str(rdap.address))
|
|
291
|
+
|
|
292
|
+
f.hosts_found = True
|
|
293
|
+
|
|
294
|
+
for ip in resolved_ips:
|
|
295
|
+
ip_obj = self._populate_ipv4(ip)
|
|
296
|
+
f.destination_ips.append(ip_obj)
|
|
297
|
+
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
except dns.resolver.NXDOMAIN:
|
|
301
|
+
# NXDOMAIN stands for Non-Existent Domain.
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
except dns.resolver.NoAnswer:
|
|
305
|
+
# The domain does exist, but the specific DNS record type you're asking for is missing.
|
|
306
|
+
break
|
|
307
|
+
|
|
308
|
+
except dns.resolver.LifetimeTimeout:
|
|
309
|
+
# The resolution lifetime expired.
|
|
310
|
+
verbose.debug('dns.resolver.LifetimeTimeout')
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
except dns.resolver.NoNameservers:
|
|
314
|
+
# If no non-broken nameservers are available to answer the question.
|
|
315
|
+
verbose.debug('dns.resolver.NoNameservers')
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
return f
|
|
319
|
+
|
|
320
|
+
def _populate_url(self, url: str) -> models.URL:
|
|
321
|
+
verbose.debug(f'Analyze {url}.')
|
|
322
|
+
|
|
323
|
+
parsed_url = urllib.parse.urlparse(url)
|
|
324
|
+
parsed_port = parsed_url.port if parsed_url.port is not None else 443 if parsed_url.scheme == 'https' else 80
|
|
325
|
+
|
|
326
|
+
u = models.URL(
|
|
327
|
+
url=url,
|
|
328
|
+
scheme=parsed_url.scheme,
|
|
329
|
+
username=parsed_url.username if parsed_url.username is not None else '',
|
|
330
|
+
password=parsed_url.password if parsed_url.password is not None else '',
|
|
331
|
+
port=parsed_port,
|
|
332
|
+
path=parsed_url.path,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
u.fqdn = self._populate_fqdn(parsed_url.hostname)
|
|
336
|
+
|
|
337
|
+
########
|
|
338
|
+
# CURL #
|
|
339
|
+
########
|
|
340
|
+
try:
|
|
341
|
+
requests.get(u.url, verify=False, timeout=2)
|
|
342
|
+
u.reachable = True
|
|
343
|
+
except requests.exceptions.RequestException:
|
|
344
|
+
u.reachable = False
|
|
345
|
+
|
|
346
|
+
return u
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
DNS_SERVERS = [
|
|
350
|
+
# Google
|
|
351
|
+
'8.8.8.8',
|
|
352
|
+
# Google
|
|
353
|
+
'8.8.4.4',
|
|
354
|
+
# Control D
|
|
355
|
+
'76.76.2.0',
|
|
356
|
+
# Control D
|
|
357
|
+
'76.76.10.0',
|
|
358
|
+
# Quad9
|
|
359
|
+
'9.9.9.9',
|
|
360
|
+
# Quad9
|
|
361
|
+
'149.112.112.112',
|
|
362
|
+
# OpenDNS Home
|
|
363
|
+
'208.67.222.222',
|
|
364
|
+
# OpenDNS Home
|
|
365
|
+
'208.67.220.220',
|
|
366
|
+
# Cloudflare
|
|
367
|
+
'1.1.1.1',
|
|
368
|
+
# Cloudflare
|
|
369
|
+
'1.0.0.1',
|
|
370
|
+
# AdGuard DNS
|
|
371
|
+
'94.140.14.14',
|
|
372
|
+
# AdGuard DNS
|
|
373
|
+
'94.140.15.15',
|
|
374
|
+
# CleanBrowsing
|
|
375
|
+
'185.228.168.9',
|
|
376
|
+
# CleanBrowsing
|
|
377
|
+
'185.228.169.9',
|
|
378
|
+
# Oracle
|
|
379
|
+
'216.146.35.35',
|
|
380
|
+
# Oracle
|
|
381
|
+
'216.146.36.36',
|
|
382
|
+
# Quad101
|
|
383
|
+
'101.101.101.101',
|
|
384
|
+
# Quad101
|
|
385
|
+
'101.102.103.104',
|
|
386
|
+
# Nippon Telegraph and Telephone
|
|
387
|
+
'129.250.35.250',
|
|
388
|
+
# Nippon Telegraph and Telephone
|
|
389
|
+
'129.250.35.251',
|
|
390
|
+
]
|
icons/404.png
ADDED
|
Binary file
|
icons/cidr.png
ADDED
|
Binary file
|
icons/fqdn.png
ADDED
|
Binary file
|
icons/ipv4.png
ADDED
|
Binary file
|
icons/target.png
ADDED
|
Binary file
|
icons/url.png
ADDED
|
Binary file
|