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 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