ptodnes 1.11.2.4__tar.gz
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.
- ptodnes-1.11.2.4/PKG-INFO +145 -0
- ptodnes-1.11.2.4/README.md +122 -0
- ptodnes-1.11.2.4/ptodnes/DNS/__init__.py +0 -0
- ptodnes-1.11.2.4/ptodnes/DNS/dns_record_dict.py +92 -0
- ptodnes-1.11.2.4/ptodnes/DNS/odnesdns.py +102 -0
- ptodnes-1.11.2.4/ptodnes/DNS/record.py +71 -0
- ptodnes-1.11.2.4/ptodnes/__init__.py +23 -0
- ptodnes-1.11.2.4/ptodnes/__main__.py +17 -0
- ptodnes-1.11.2.4/ptodnes/configprovider/__init__.py +0 -0
- ptodnes-1.11.2.4/ptodnes/configprovider/configprovider.py +48 -0
- ptodnes-1.11.2.4/ptodnes/dataexporter/__init__.py +89 -0
- ptodnes-1.11.2.4/ptodnes/datasources/__init__.py +29 -0
- ptodnes-1.11.2.4/ptodnes/datasources/crtsh.py +86 -0
- ptodnes-1.11.2.4/ptodnes/datasources/datasource.py +185 -0
- ptodnes-1.11.2.4/ptodnes/datasources/securitytrails.py +169 -0
- ptodnes-1.11.2.4/ptodnes/datasources/shodan.py +154 -0
- ptodnes-1.11.2.4/ptodnes/datasources/virustotal.py +159 -0
- ptodnes-1.11.2.4/ptodnes/datasources/wordlist.py +87 -0
- ptodnes-1.11.2.4/ptodnes/metaclasses/__init__.py +10 -0
- ptodnes-1.11.2.4/ptodnes/process.py +135 -0
- ptodnes-1.11.2.4/ptodnes/ptodnes.py +168 -0
- ptodnes-1.11.2.4/pyproject.toml +41 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ptodnes
|
|
3
|
+
Version: 1.11.2.4
|
|
4
|
+
Summary:
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Ondrej Dohnal
|
|
7
|
+
Author-email: xdohna45@vutbr.cz
|
|
8
|
+
Requires-Python: >3.11,<=3.15
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: aiodns (>=3.2.0,<4.0.0)
|
|
15
|
+
Requires-Dist: aiofiles (>=25.1.0,<26.0.0)
|
|
16
|
+
Requires-Dist: aiohttp (>=3.11.13,<4.0.0)
|
|
17
|
+
Requires-Dist: aiopg (>=1.4.0,<2.0.0)
|
|
18
|
+
Requires-Dist: ptlibs (>=1.0.17,<2.0.0)
|
|
19
|
+
Requires-Dist: punycode (>=0.2.1,<0.3.0)
|
|
20
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
[](https://www.penterep.com/)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## ptodnes - OSINT Domain Name Enumeration System
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
1. Download latest release whl package from [releases](https://github.com/Penterep/ptodnes/releases/latest) page.
|
|
32
|
+
2. Install the package using pip/pipx.
|
|
33
|
+
```bash
|
|
34
|
+
pipx install <path_to_downloaded_whl_file>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
```bash
|
|
39
|
+
pipx install ~/Downloads/ptodnes-1.11.1-py3-none-any.whl
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Adding to PATH
|
|
43
|
+
If you're unable to invoke the script from your terminal, it's likely because it's not included in your PATH. You can resolve this issue by executing the following commands, depending on the shell you're using:
|
|
44
|
+
|
|
45
|
+
For Bash Users
|
|
46
|
+
```bash
|
|
47
|
+
echo "export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"" >> ~/.bashrc
|
|
48
|
+
source ~/.bashrc
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For ZSH Users
|
|
52
|
+
```bash
|
|
53
|
+
echo "export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"" >> ~/.zshrc
|
|
54
|
+
source ~/.zshrc
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage examples
|
|
58
|
+
```
|
|
59
|
+
ptodnes -l
|
|
60
|
+
ptodnes -d example.com
|
|
61
|
+
ptodnes -d example.com example.net
|
|
62
|
+
ptodnes -d example.com -D VirusTotal CRTsh
|
|
63
|
+
ptodnes -d example.com -j -o example -t A AAAA
|
|
64
|
+
ptodnes -d example.com -D Wordlist -w /usr/share/wordlists/rockyou.txt
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Options
|
|
68
|
+
```
|
|
69
|
+
-c --csv Output in CSV format
|
|
70
|
+
-C --config <config> Path to config file (default ~/ptodnes.toml)
|
|
71
|
+
-d --domain <domain ...> Domains to search for
|
|
72
|
+
-D --datasource <datasource ...> Datasources to browse
|
|
73
|
+
-e --exclude-unverified Exclude unverified records
|
|
74
|
+
-j --json Output in JSON format
|
|
75
|
+
-l --list List available datasources
|
|
76
|
+
-n --nonxdomain Filter results with no DNS data
|
|
77
|
+
-o --output <file_prefix> Save results to files (format specification required)
|
|
78
|
+
-p --ptjson Output in ptJSONlib format
|
|
79
|
+
-q --query Query domains against DNS servers
|
|
80
|
+
-r --retry <count> Number of attempts (default:5)
|
|
81
|
+
-t --type <type ...> Types of DNS records to search for
|
|
82
|
+
-T --timeout <timeout> Datasource connection timeout (in seconds, default:5)
|
|
83
|
+
-v --version Print version and exit
|
|
84
|
+
-V --verbose <1|2|3|4> Set verbosity level (1=ERROR, 2=WARNING, 3=INFO, 4=DEBUG)
|
|
85
|
+
-w --wordlist <wordlist ...> Path to wordlist(s) for wordlist search.
|
|
86
|
+
-y --yaml Output in YAML format
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
Configuration is stored in TOML file. Default location is `~/ptodnes.toml`.
|
|
92
|
+
|
|
93
|
+
Example configuration:
|
|
94
|
+
```toml
|
|
95
|
+
[VirusTotal]
|
|
96
|
+
api_keys = [
|
|
97
|
+
'API_KEY_1',
|
|
98
|
+
'API_KEY_2'
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
[SecurityTrails]
|
|
102
|
+
api_keys = [
|
|
103
|
+
'API_KEY_1',
|
|
104
|
+
'API_KEY_2'
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
[Wordlist]
|
|
108
|
+
wordlists = [
|
|
109
|
+
'/usr/share/wordlists/seclists/Discovery/DNS/namelist.txt'
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
...
|
|
113
|
+
[<Datasource>]
|
|
114
|
+
api_keys = [
|
|
115
|
+
'...'
|
|
116
|
+
]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Dependencies
|
|
120
|
+
```
|
|
121
|
+
ptlibs
|
|
122
|
+
aiodns
|
|
123
|
+
aiohttp
|
|
124
|
+
aiopg
|
|
125
|
+
pyyaml
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
Copyright (c) 2025 Penterep Security s.r.o.
|
|
131
|
+
|
|
132
|
+
ptodnes is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
133
|
+
|
|
134
|
+
ptodnes is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
135
|
+
|
|
136
|
+
You should have received a copy of the GNU General Public License along with ptodnes. If not, see https://www.gnu.org/licenses/.
|
|
137
|
+
|
|
138
|
+
## Warning
|
|
139
|
+
|
|
140
|
+
You are only allowed to run the tool against the websites which
|
|
141
|
+
you have been given permission to pentest. We do not accept any
|
|
142
|
+
responsibility for any damage/harm that this application causes to your
|
|
143
|
+
computer, or your network. Penterep is not responsible for any illegal
|
|
144
|
+
or malicious use of this code. Be Ethical!
|
|
145
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
[](https://www.penterep.com/)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## ptodnes - OSINT Domain Name Enumeration System
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
1. Download latest release whl package from [releases](https://github.com/Penterep/ptodnes/releases/latest) page.
|
|
10
|
+
2. Install the package using pip/pipx.
|
|
11
|
+
```bash
|
|
12
|
+
pipx install <path_to_downloaded_whl_file>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
```bash
|
|
17
|
+
pipx install ~/Downloads/ptodnes-1.11.1-py3-none-any.whl
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Adding to PATH
|
|
21
|
+
If you're unable to invoke the script from your terminal, it's likely because it's not included in your PATH. You can resolve this issue by executing the following commands, depending on the shell you're using:
|
|
22
|
+
|
|
23
|
+
For Bash Users
|
|
24
|
+
```bash
|
|
25
|
+
echo "export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"" >> ~/.bashrc
|
|
26
|
+
source ~/.bashrc
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For ZSH Users
|
|
30
|
+
```bash
|
|
31
|
+
echo "export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"" >> ~/.zshrc
|
|
32
|
+
source ~/.zshrc
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage examples
|
|
36
|
+
```
|
|
37
|
+
ptodnes -l
|
|
38
|
+
ptodnes -d example.com
|
|
39
|
+
ptodnes -d example.com example.net
|
|
40
|
+
ptodnes -d example.com -D VirusTotal CRTsh
|
|
41
|
+
ptodnes -d example.com -j -o example -t A AAAA
|
|
42
|
+
ptodnes -d example.com -D Wordlist -w /usr/share/wordlists/rockyou.txt
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Options
|
|
46
|
+
```
|
|
47
|
+
-c --csv Output in CSV format
|
|
48
|
+
-C --config <config> Path to config file (default ~/ptodnes.toml)
|
|
49
|
+
-d --domain <domain ...> Domains to search for
|
|
50
|
+
-D --datasource <datasource ...> Datasources to browse
|
|
51
|
+
-e --exclude-unverified Exclude unverified records
|
|
52
|
+
-j --json Output in JSON format
|
|
53
|
+
-l --list List available datasources
|
|
54
|
+
-n --nonxdomain Filter results with no DNS data
|
|
55
|
+
-o --output <file_prefix> Save results to files (format specification required)
|
|
56
|
+
-p --ptjson Output in ptJSONlib format
|
|
57
|
+
-q --query Query domains against DNS servers
|
|
58
|
+
-r --retry <count> Number of attempts (default:5)
|
|
59
|
+
-t --type <type ...> Types of DNS records to search for
|
|
60
|
+
-T --timeout <timeout> Datasource connection timeout (in seconds, default:5)
|
|
61
|
+
-v --version Print version and exit
|
|
62
|
+
-V --verbose <1|2|3|4> Set verbosity level (1=ERROR, 2=WARNING, 3=INFO, 4=DEBUG)
|
|
63
|
+
-w --wordlist <wordlist ...> Path to wordlist(s) for wordlist search.
|
|
64
|
+
-y --yaml Output in YAML format
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
Configuration is stored in TOML file. Default location is `~/ptodnes.toml`.
|
|
70
|
+
|
|
71
|
+
Example configuration:
|
|
72
|
+
```toml
|
|
73
|
+
[VirusTotal]
|
|
74
|
+
api_keys = [
|
|
75
|
+
'API_KEY_1',
|
|
76
|
+
'API_KEY_2'
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[SecurityTrails]
|
|
80
|
+
api_keys = [
|
|
81
|
+
'API_KEY_1',
|
|
82
|
+
'API_KEY_2'
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[Wordlist]
|
|
86
|
+
wordlists = [
|
|
87
|
+
'/usr/share/wordlists/seclists/Discovery/DNS/namelist.txt'
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
...
|
|
91
|
+
[<Datasource>]
|
|
92
|
+
api_keys = [
|
|
93
|
+
'...'
|
|
94
|
+
]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Dependencies
|
|
98
|
+
```
|
|
99
|
+
ptlibs
|
|
100
|
+
aiodns
|
|
101
|
+
aiohttp
|
|
102
|
+
aiopg
|
|
103
|
+
pyyaml
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
Copyright (c) 2025 Penterep Security s.r.o.
|
|
109
|
+
|
|
110
|
+
ptodnes is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
111
|
+
|
|
112
|
+
ptodnes is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
113
|
+
|
|
114
|
+
You should have received a copy of the GNU General Public License along with ptodnes. If not, see https://www.gnu.org/licenses/.
|
|
115
|
+
|
|
116
|
+
## Warning
|
|
117
|
+
|
|
118
|
+
You are only allowed to run the tool against the websites which
|
|
119
|
+
you have been given permission to pentest. We do not accept any
|
|
120
|
+
responsibility for any damage/harm that this application causes to your
|
|
121
|
+
computer, or your network. Penterep is not responsible for any illegal
|
|
122
|
+
or malicious use of this code. Be Ethical!
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Generator, List
|
|
2
|
+
|
|
3
|
+
from ptodnes.DNS.record import DNSRecord
|
|
4
|
+
from ptodnes.datasources.datasource import DatasourceObject
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DNSRecordDict(dict[str, list[DNSRecord]]):
|
|
8
|
+
"""
|
|
9
|
+
DNS Records dictionary
|
|
10
|
+
"""
|
|
11
|
+
def append(self, item: DatasourceObject):
|
|
12
|
+
"""
|
|
13
|
+
Add DNS record to dictionary. Checks if record exists.
|
|
14
|
+
:param item: DNS record
|
|
15
|
+
:return:
|
|
16
|
+
"""
|
|
17
|
+
if item not in self.keys():
|
|
18
|
+
self[item.domain] = list(set(item.DNSData))
|
|
19
|
+
else:
|
|
20
|
+
for obj in item.DNSData:
|
|
21
|
+
if obj not in self[item.domain]:
|
|
22
|
+
self[item.domain].append(obj)
|
|
23
|
+
else:
|
|
24
|
+
for i in range(len(self[item.domain])):
|
|
25
|
+
if self[item.domain][i] == obj:
|
|
26
|
+
self[item.domain][i].source.update(obj.source)
|
|
27
|
+
|
|
28
|
+
def extend(self, items: list[DatasourceObject]):
|
|
29
|
+
"""
|
|
30
|
+
Add DNS records to dictionary
|
|
31
|
+
:param items: DNS Records
|
|
32
|
+
:return:
|
|
33
|
+
"""
|
|
34
|
+
for item in items:
|
|
35
|
+
self.append(item)
|
|
36
|
+
def filter(self, types: list):
|
|
37
|
+
"""
|
|
38
|
+
Filter DNS records by type
|
|
39
|
+
:param types: types to filter
|
|
40
|
+
:return:
|
|
41
|
+
"""
|
|
42
|
+
keys = []
|
|
43
|
+
filter_types = types.copy()
|
|
44
|
+
if 'ANY' in filter_types:
|
|
45
|
+
return
|
|
46
|
+
self.filterNX()
|
|
47
|
+
for key, value in self.items():
|
|
48
|
+
filtered = [x for x in filter((lambda i: i.type in filter_types), value)]
|
|
49
|
+
if not filtered:
|
|
50
|
+
keys.append(key)
|
|
51
|
+
self[key] = filtered
|
|
52
|
+
for key in keys:
|
|
53
|
+
del (self[key])
|
|
54
|
+
|
|
55
|
+
def filter_untrusted(self):
|
|
56
|
+
"""
|
|
57
|
+
Filter records that have not been verified
|
|
58
|
+
:return:
|
|
59
|
+
"""
|
|
60
|
+
keys = []
|
|
61
|
+
for key, value in self.items():
|
|
62
|
+
filtered = [x for x in filter((lambda i: i.verified), value)]
|
|
63
|
+
if not filtered:
|
|
64
|
+
keys.append(key)
|
|
65
|
+
self[key] = filtered
|
|
66
|
+
for key in keys:
|
|
67
|
+
del (self[key])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def filterNX(self):
|
|
71
|
+
"""
|
|
72
|
+
Filter out domains without any record
|
|
73
|
+
:return:
|
|
74
|
+
"""
|
|
75
|
+
keys = []
|
|
76
|
+
for key, value in self.items():
|
|
77
|
+
|
|
78
|
+
while DNSRecord('<NONE>',0,'<EMPTY>',False, {''}, None) in value:
|
|
79
|
+
value.remove(DNSRecord('<NONE>',0,'<EMPTY>',False, {''}, None))
|
|
80
|
+
if not value:
|
|
81
|
+
keys.append(key)
|
|
82
|
+
|
|
83
|
+
for key in keys:
|
|
84
|
+
del(self[key])
|
|
85
|
+
|
|
86
|
+
def seq(self) -> Generator[DatasourceObject]:
|
|
87
|
+
for key in self.keys():
|
|
88
|
+
do = DatasourceObject(domain=key, DNSData=self[key])
|
|
89
|
+
yield do
|
|
90
|
+
|
|
91
|
+
def as_list(self) -> List[DatasourceObject]:
|
|
92
|
+
return list(self.seq())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import re
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
import aiodns
|
|
5
|
+
from ptodnes.DNS.record import DNSRecord
|
|
6
|
+
from ptodnes.DNS.dns_record_dict import DNSRecordDict
|
|
7
|
+
from ptodnes.datasources.datasource import DNSRecordGenerator, DatasourceObject
|
|
8
|
+
from ptodnes.metaclasses import Singleton
|
|
9
|
+
|
|
10
|
+
class OdnesDNS(metaclass=Singleton):
|
|
11
|
+
"""
|
|
12
|
+
Class to provide DNS queries
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, loop):
|
|
15
|
+
self.__resolver = aiodns.DNSResolver(loop=loop)
|
|
16
|
+
self.__rev4 = re.compile(r"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$")
|
|
17
|
+
self.__rev6 = re.compile(r'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))', re.IGNORECASE)
|
|
18
|
+
|
|
19
|
+
async def reverse(self, ip: str) -> list[str]:
|
|
20
|
+
if self.__rev4.match(ip) or self.__rev6.match(ip):
|
|
21
|
+
try:
|
|
22
|
+
res = await self.__resolver.gethostbyaddr(ip)
|
|
23
|
+
return res.aliases
|
|
24
|
+
except aiodns.error.DNSError:
|
|
25
|
+
return []
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
async def query_one(self, domain: str, domain_data: list[DNSRecord], qtype='ANY', *, print_func=None):
|
|
29
|
+
"""
|
|
30
|
+
Query one domain with selected record type, update domain_data with results.
|
|
31
|
+
:param domain: domain to query.
|
|
32
|
+
:param domain_data: domain data to update.
|
|
33
|
+
:param qtype: query type.
|
|
34
|
+
:param print_func: output function.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
if print_func:
|
|
38
|
+
print_func(f"querying {domain}", clear_to_eol=True, end='\r')
|
|
39
|
+
data = await self.__resolver.query(domain, qtype) #ANY not working on all servers
|
|
40
|
+
preprocessed = []
|
|
41
|
+
if type(data) is not type([]):
|
|
42
|
+
preprocessed.append(data)
|
|
43
|
+
else:
|
|
44
|
+
preprocessed = data
|
|
45
|
+
results = {}
|
|
46
|
+
for response in preprocessed:
|
|
47
|
+
record: DNSRecord
|
|
48
|
+
if response.type in ['A', 'AAAA', 'NS']:
|
|
49
|
+
record = DNSRecordGenerator(type=response.type, source="DNS", value=response.host, ttl=response.ttl,
|
|
50
|
+
verified=True, record_last_seen = datetime.now(tz=timezone.utc))
|
|
51
|
+
elif response.type in ['CNAME']:
|
|
52
|
+
record = DNSRecordGenerator(type=response.type, source="DNS", value=response.cname,
|
|
53
|
+
ttl=response.ttl, verified=True, record_last_seen = datetime.now(tz=timezone.utc))
|
|
54
|
+
elif response.type in ['MX']:
|
|
55
|
+
record = DNSRecordGenerator(type=response.type, source="DNS", value=response.host, ttl=response.ttl,
|
|
56
|
+
priority=response.priority, verified=True,
|
|
57
|
+
record_last_seen = datetime.now(tz=timezone.utc))
|
|
58
|
+
elif response.type in ['PTR']:
|
|
59
|
+
record = DNSRecordGenerator(type=response.type, source="DNS", value=response.name, ttl=response.ttl,
|
|
60
|
+
verified=True, record_last_seen = datetime.now(tz=timezone.utc))
|
|
61
|
+
elif response.type in ['SOA']:
|
|
62
|
+
record = DNSRecordGenerator(type=response.type, source="DNS", value=response.nsname, ttl=response.ttl,
|
|
63
|
+
verified=True, rname=response.hostmaster, retry=response.retry,
|
|
64
|
+
expire=response.expires, refresh=response.refresh,
|
|
65
|
+
serial=response.serial, minimum=response.minttl,
|
|
66
|
+
record_last_seen = datetime.now(tz=timezone.utc))
|
|
67
|
+
elif response.type in ['SRV']:
|
|
68
|
+
record = DNSRecordGenerator(type=response.type, source="DNS", value=response.host, ttl=response.ttl,
|
|
69
|
+
verified=True, record_last_seen = datetime.now(tz=timezone.utc))
|
|
70
|
+
elif response.type in ['TXT']:
|
|
71
|
+
record = DNSRecordGenerator(type=response.type, source="DNS", value=response.text, ttl=response.ttl,
|
|
72
|
+
verified=True, record_last_seen = datetime.now(tz=timezone.utc))
|
|
73
|
+
else:
|
|
74
|
+
continue
|
|
75
|
+
sources = set()
|
|
76
|
+
if DNSRecord('<NONE>',0,'<EMPTY>',False, {''}, None) in domain_data:
|
|
77
|
+
for empty in domain_data:
|
|
78
|
+
sources.update(empty.source)
|
|
79
|
+
record.source.update(sources)
|
|
80
|
+
|
|
81
|
+
if record not in domain_data:
|
|
82
|
+
domain_data.append(record)
|
|
83
|
+
else:
|
|
84
|
+
for i in range(len(domain_data)):
|
|
85
|
+
if domain_data[i] == record:
|
|
86
|
+
record.source.update(domain_data[i].source)
|
|
87
|
+
domain_data[i] = record
|
|
88
|
+
except aiodns.error.DNSError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
async def query(self, domain_list: DNSRecordDict, qtype='ANY', *, print_func=None):
|
|
92
|
+
"""
|
|
93
|
+
Query provided domain list with selected record type, update its data with results.
|
|
94
|
+
:param domain_list: domain list to query.
|
|
95
|
+
:param qtype: query type.
|
|
96
|
+
"""
|
|
97
|
+
tasks = []
|
|
98
|
+
for domain, info in domain_list.items():
|
|
99
|
+
task = asyncio.create_task(self.query_one(domain, info, qtype, print_func=print_func))
|
|
100
|
+
tasks.append(task)
|
|
101
|
+
await asyncio.gather(*tasks)
|
|
102
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class DNSRecord:
|
|
7
|
+
"""
|
|
8
|
+
Base class for DNS record
|
|
9
|
+
"""
|
|
10
|
+
type: str
|
|
11
|
+
ttl: int
|
|
12
|
+
value: str
|
|
13
|
+
verified: bool
|
|
14
|
+
source: set[str]
|
|
15
|
+
record_last_seen: datetime | None
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def dict_factory(x):
|
|
19
|
+
exclude_fields = ("ttl",)
|
|
20
|
+
return {k: (v.isoformat() if isinstance(v, datetime) else list(v) if isinstance(v, set) else v) for (k, v) in x if ((v is not None) and (k not in exclude_fields))}
|
|
21
|
+
|
|
22
|
+
def __eq__(self, other):
|
|
23
|
+
return self.type == other.type and self.value == other.value
|
|
24
|
+
|
|
25
|
+
def __hash__(self):
|
|
26
|
+
return hash((self.type, self.value))
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class SOARecord(DNSRecord):
|
|
30
|
+
"""
|
|
31
|
+
Class for SOA record
|
|
32
|
+
"""
|
|
33
|
+
rname: str = None
|
|
34
|
+
retry: int = None
|
|
35
|
+
minimum: int = None
|
|
36
|
+
refresh: int = None
|
|
37
|
+
expire: int = None
|
|
38
|
+
serial: int = None
|
|
39
|
+
|
|
40
|
+
def __eq__(self, other):
|
|
41
|
+
return super().__eq__(other)
|
|
42
|
+
|
|
43
|
+
def __hash__(self):
|
|
44
|
+
return super().__hash__()
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class MXRecord(DNSRecord):
|
|
48
|
+
"""
|
|
49
|
+
Class for MX record
|
|
50
|
+
"""
|
|
51
|
+
priority: int = 0
|
|
52
|
+
|
|
53
|
+
def __eq__(self, other):
|
|
54
|
+
return super().__eq__(other)
|
|
55
|
+
|
|
56
|
+
def __hash__(self):
|
|
57
|
+
return super().__hash__()
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class CAARecord(DNSRecord):
|
|
61
|
+
"""
|
|
62
|
+
Class for CAA record
|
|
63
|
+
"""
|
|
64
|
+
flag: int = None
|
|
65
|
+
tag: str = None
|
|
66
|
+
|
|
67
|
+
def __eq__(self, other):
|
|
68
|
+
return super().__eq__(other)
|
|
69
|
+
|
|
70
|
+
def __hash__(self):
|
|
71
|
+
return super().__hash__()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import signal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def add_signal_handlers():
|
|
6
|
+
"""
|
|
7
|
+
Properly handles SIGINT and SIGTERM signals. Ensures correct end of all coroutines.
|
|
8
|
+
:return:
|
|
9
|
+
"""
|
|
10
|
+
loop = asyncio.get_event_loop()
|
|
11
|
+
|
|
12
|
+
async def shutdown(_: signal.Signals) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Cancel all running async tasks (other than this one) when called.
|
|
15
|
+
By catching asyncio.CancelledError, any running task can perform
|
|
16
|
+
any necessary cleanup when it's cancelled.
|
|
17
|
+
"""
|
|
18
|
+
for task in asyncio.all_tasks(loop):
|
|
19
|
+
if task is not asyncio.current_task(loop):
|
|
20
|
+
task.cancel()
|
|
21
|
+
|
|
22
|
+
for sig in [signal.SIGINT, signal.SIGTERM]:
|
|
23
|
+
loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown(sig)))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import asyncio
|
|
3
|
+
from ptodnes.ptodnes import main
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __main__():
|
|
7
|
+
if sys.platform == 'win32':
|
|
8
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
9
|
+
loop = asyncio.new_event_loop()
|
|
10
|
+
try:
|
|
11
|
+
loop.run_until_complete(main(loop))
|
|
12
|
+
finally:
|
|
13
|
+
loop.close()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
__main__()
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
import pathlib
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from ptodnes.metaclasses import Singleton
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigProvider(metaclass=Singleton):
|
|
9
|
+
"""
|
|
10
|
+
Singleton class that provides access to configuration settings.
|
|
11
|
+
"""
|
|
12
|
+
@property
|
|
13
|
+
def config_file(self):
|
|
14
|
+
"""
|
|
15
|
+
Config file location.
|
|
16
|
+
"""
|
|
17
|
+
return self.__config_file
|
|
18
|
+
|
|
19
|
+
@config_file.setter
|
|
20
|
+
def config_file(self, value):
|
|
21
|
+
self.__config_file = value
|
|
22
|
+
if not pathlib.Path.exists(pathlib.Path(value)):
|
|
23
|
+
with open(value, "a", encoding="utf8", newline="") as _: pass
|
|
24
|
+
with open(value, "rb") as f: self.__config = tomllib.load(f)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def config(self):
|
|
28
|
+
"""
|
|
29
|
+
Configuration read from configuration file.
|
|
30
|
+
"""
|
|
31
|
+
return self.__config
|
|
32
|
+
|
|
33
|
+
def __init__(self, config_file = os.path.join(pathlib.Path.home(), "ptodnes.toml")):
|
|
34
|
+
"""
|
|
35
|
+
:param config_file: config file path
|
|
36
|
+
"""
|
|
37
|
+
self.__config_file = config_file
|
|
38
|
+
if not pathlib.Path.exists(pathlib.Path(config_file)):
|
|
39
|
+
with open(config_file, "a", encoding="utf8", newline="") as _: pass
|
|
40
|
+
with open(config_file, "rb") as f: self.__config = tomllib.load(f)
|
|
41
|
+
|
|
42
|
+
def get_config(self, section: str) -> dict:
|
|
43
|
+
"""
|
|
44
|
+
Get config for specific section.
|
|
45
|
+
:param section: section name
|
|
46
|
+
:return: config
|
|
47
|
+
"""
|
|
48
|
+
return self.__config.get(section, {})
|