secator 0.5.2__py3-none-any.whl → 0.7.0__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.
Potentially problematic release.
This version of secator might be problematic. Click here for more details.
- secator/celery.py +160 -185
- secator/celery_utils.py +268 -0
- secator/cli.py +327 -106
- secator/config.py +27 -11
- secator/configs/workflows/host_recon.yaml +5 -3
- secator/configs/workflows/port_scan.yaml +7 -3
- secator/configs/workflows/url_bypass.yaml +10 -0
- secator/configs/workflows/url_vuln.yaml +1 -1
- secator/decorators.py +169 -92
- secator/definitions.py +10 -3
- secator/exporters/__init__.py +7 -5
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +27 -19
- secator/exporters/gdrive.py +16 -11
- secator/exporters/json.py +3 -1
- secator/exporters/table.py +30 -2
- secator/exporters/txt.py +20 -16
- secator/hooks/gcs.py +53 -0
- secator/hooks/mongodb.py +54 -28
- secator/output_types/__init__.py +29 -11
- secator/output_types/_base.py +11 -1
- secator/output_types/error.py +36 -0
- secator/output_types/exploit.py +1 -1
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +7 -0
- secator/output_types/port.py +8 -1
- secator/output_types/progress.py +6 -1
- secator/output_types/record.py +3 -1
- secator/output_types/stat.py +33 -0
- secator/output_types/tag.py +6 -4
- secator/output_types/url.py +6 -3
- secator/output_types/vulnerability.py +3 -2
- secator/output_types/warning.py +24 -0
- secator/report.py +55 -23
- secator/rich.py +44 -39
- secator/runners/_base.py +622 -635
- secator/runners/_helpers.py +5 -91
- secator/runners/celery.py +18 -0
- secator/runners/command.py +364 -211
- secator/runners/scan.py +8 -24
- secator/runners/task.py +21 -55
- secator/runners/workflow.py +41 -40
- secator/scans/__init__.py +28 -0
- secator/serializers/dataclass.py +6 -0
- secator/serializers/json.py +10 -5
- secator/serializers/regex.py +12 -4
- secator/tasks/_categories.py +6 -3
- secator/tasks/bbot.py +293 -0
- secator/tasks/bup.py +98 -0
- secator/tasks/cariddi.py +38 -49
- secator/tasks/dalfox.py +3 -0
- secator/tasks/dirsearch.py +12 -23
- secator/tasks/dnsx.py +49 -30
- secator/tasks/dnsxbrute.py +2 -0
- secator/tasks/feroxbuster.py +8 -17
- secator/tasks/ffuf.py +3 -2
- secator/tasks/fping.py +3 -3
- secator/tasks/gau.py +5 -0
- secator/tasks/gf.py +2 -2
- secator/tasks/gospider.py +4 -0
- secator/tasks/grype.py +9 -9
- secator/tasks/h8mail.py +31 -41
- secator/tasks/httpx.py +58 -21
- secator/tasks/katana.py +18 -22
- secator/tasks/maigret.py +26 -24
- secator/tasks/mapcidr.py +2 -3
- secator/tasks/msfconsole.py +4 -16
- secator/tasks/naabu.py +3 -1
- secator/tasks/nmap.py +50 -35
- secator/tasks/nuclei.py +9 -2
- secator/tasks/searchsploit.py +17 -9
- secator/tasks/subfinder.py +5 -1
- secator/tasks/wpscan.py +79 -93
- secator/template.py +61 -45
- secator/thread.py +24 -0
- secator/utils.py +330 -80
- secator/utils_test.py +48 -23
- secator/workflows/__init__.py +28 -0
- {secator-0.5.2.dist-info → secator-0.7.0.dist-info}/METADATA +12 -6
- secator-0.7.0.dist-info/RECORD +115 -0
- {secator-0.5.2.dist-info → secator-0.7.0.dist-info}/WHEEL +1 -1
- secator-0.5.2.dist-info/RECORD +0 -101
- {secator-0.5.2.dist-info → secator-0.7.0.dist-info}/entry_points.txt +0 -0
- {secator-0.5.2.dist-info → secator-0.7.0.dist-info}/licenses/LICENSE +0 -0
secator/tasks/dnsxbrute.py
CHANGED
|
@@ -2,6 +2,7 @@ from secator.decorators import task
|
|
|
2
2
|
from secator.definitions import (DOMAIN, HOST, RATE_LIMIT, RETRIES, THREADS, WORDLIST, EXTRA_DATA)
|
|
3
3
|
from secator.config import CONFIG
|
|
4
4
|
from secator.output_types import Subdomain
|
|
5
|
+
from secator.serializers import JSONSerializer
|
|
5
6
|
from secator.tasks._categories import ReconDns
|
|
6
7
|
|
|
7
8
|
|
|
@@ -21,6 +22,7 @@ class dnsxbrute(ReconDns):
|
|
|
21
22
|
WORDLIST: {'type': str, 'short': 'w', 'default': CONFIG.wordlists.defaults.dns, 'help': 'Wordlist'},
|
|
22
23
|
'trace': {'is_flag': True, 'default': False, 'help': 'Perform dns tracing'},
|
|
23
24
|
}
|
|
25
|
+
item_loaders = [JSONSerializer()]
|
|
24
26
|
output_map = {
|
|
25
27
|
Subdomain: {
|
|
26
28
|
HOST: 'host',
|
secator/tasks/feroxbuster.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import shlex
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
1
|
from secator.decorators import task
|
|
5
2
|
from secator.definitions import (CONTENT_TYPE, DELAY, DEPTH, FILTER_CODES,
|
|
6
3
|
FILTER_REGEX, FILTER_SIZE, FILTER_WORDS,
|
|
7
4
|
FOLLOW_REDIRECT, HEADER, LINES, MATCH_CODES,
|
|
8
5
|
MATCH_REGEX, MATCH_SIZE, MATCH_WORDS, METHOD,
|
|
9
|
-
OPT_NOT_SUPPORTED, OPT_PIPE_INPUT,
|
|
6
|
+
OPT_NOT_SUPPORTED, OPT_PIPE_INPUT, PROXY,
|
|
10
7
|
RATE_LIMIT, RETRIES, STATUS_CODE,
|
|
11
8
|
THREADS, TIMEOUT, USER_AGENT, WORDLIST, WORDS, DEFAULT_FEROXBUSTER_FLAGS)
|
|
12
9
|
from secator.output_types import Url
|
|
10
|
+
from secator.serializers import JSONSerializer
|
|
13
11
|
from secator.tasks._categories import HttpFuzzer
|
|
14
12
|
|
|
15
13
|
|
|
@@ -20,7 +18,7 @@ class feroxbuster(HttpFuzzer):
|
|
|
20
18
|
input_flag = '--url'
|
|
21
19
|
input_chunk_size = 1
|
|
22
20
|
file_flag = OPT_PIPE_INPUT
|
|
23
|
-
json_flag = '--json'
|
|
21
|
+
json_flag = '--silent --json'
|
|
24
22
|
opt_prefix = '--'
|
|
25
23
|
opts = {
|
|
26
24
|
# 'auto_tune': {'is_flag': True, 'default': False, 'help': 'Automatically lower scan rate when too many errors'},
|
|
@@ -51,6 +49,7 @@ class feroxbuster(HttpFuzzer):
|
|
|
51
49
|
USER_AGENT: 'user-agent',
|
|
52
50
|
WORDLIST: 'wordlist'
|
|
53
51
|
}
|
|
52
|
+
item_loaders = [JSONSerializer()]
|
|
54
53
|
output_map = {
|
|
55
54
|
Url: {
|
|
56
55
|
STATUS_CODE: 'status',
|
|
@@ -70,21 +69,13 @@ class feroxbuster(HttpFuzzer):
|
|
|
70
69
|
proxy_http = True
|
|
71
70
|
profile = 'cpu'
|
|
72
71
|
|
|
73
|
-
@staticmethod
|
|
74
|
-
def on_init(self):
|
|
75
|
-
self.output_path = self.get_opt_value(OUTPUT_PATH)
|
|
76
|
-
if not self.output_path:
|
|
77
|
-
self.output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
78
|
-
Path(self.output_path).touch()
|
|
79
|
-
self.cmd += f' --output {self.output_path}'
|
|
80
|
-
|
|
81
72
|
@staticmethod
|
|
82
73
|
def on_start(self):
|
|
83
|
-
if self.
|
|
74
|
+
if self.inputs_path:
|
|
84
75
|
self.cmd += ' --stdin'
|
|
85
|
-
self.cmd += f' & tail --pid=$! -f {shlex.quote(self.output_path)}'
|
|
86
|
-
self.shell = True
|
|
87
76
|
|
|
88
77
|
@staticmethod
|
|
89
78
|
def validate_item(self, item):
|
|
90
|
-
|
|
79
|
+
if isinstance(item, dict):
|
|
80
|
+
return item['type'] == 'response'
|
|
81
|
+
return True
|
secator/tasks/ffuf.py
CHANGED
|
@@ -25,7 +25,7 @@ class ffuf(HttpFuzzer):
|
|
|
25
25
|
json_flag = '-json'
|
|
26
26
|
version_flag = '-V'
|
|
27
27
|
item_loaders = [
|
|
28
|
-
JSONSerializer(),
|
|
28
|
+
JSONSerializer(strict=True),
|
|
29
29
|
RegexSerializer(FFUF_PROGRESS_REGEX, fields=['count', 'total', 'rps', 'duration', 'errors'])
|
|
30
30
|
]
|
|
31
31
|
opts = {
|
|
@@ -79,5 +79,6 @@ class ffuf(HttpFuzzer):
|
|
|
79
79
|
|
|
80
80
|
@staticmethod
|
|
81
81
|
def on_item(self, item):
|
|
82
|
-
item
|
|
82
|
+
if isinstance(item, Url):
|
|
83
|
+
item.method = self.get_opt_value(METHOD) or 'GET'
|
|
83
84
|
return item
|
secator/tasks/fping.py
CHANGED
|
@@ -33,9 +33,9 @@ class fping(ReconIp):
|
|
|
33
33
|
|
|
34
34
|
@staticmethod
|
|
35
35
|
def item_loader(self, line):
|
|
36
|
-
if validators.ipv4(line) or validators.ipv6(line):
|
|
37
|
-
return
|
|
38
|
-
|
|
36
|
+
if not (validators.ipv4(line) or validators.ipv6(line)):
|
|
37
|
+
return
|
|
38
|
+
yield {'ip': line, 'alive': True}
|
|
39
39
|
|
|
40
40
|
@staticmethod
|
|
41
41
|
def on_line(self, line):
|
secator/tasks/gau.py
CHANGED
|
@@ -5,6 +5,7 @@ from secator.definitions import (DELAY, DEPTH, FILTER_CODES, FILTER_REGEX,
|
|
|
5
5
|
MATCH_WORDS, METHOD, OPT_NOT_SUPPORTED,
|
|
6
6
|
OPT_PIPE_INPUT, PROXY, RATE_LIMIT, RETRIES,
|
|
7
7
|
THREADS, TIMEOUT, USER_AGENT)
|
|
8
|
+
from secator.serializers import JSONSerializer
|
|
8
9
|
from secator.tasks._categories import HttpCrawler
|
|
9
10
|
|
|
10
11
|
|
|
@@ -15,6 +16,9 @@ class gau(HttpCrawler):
|
|
|
15
16
|
file_flag = OPT_PIPE_INPUT
|
|
16
17
|
json_flag = '--json'
|
|
17
18
|
opt_prefix = '--'
|
|
19
|
+
opts = {
|
|
20
|
+
'providers': {'type': str, 'default': None, 'help': 'List of providers to use (wayback,commoncrawl,otx,urlscan)'}
|
|
21
|
+
}
|
|
18
22
|
opt_key_map = {
|
|
19
23
|
HEADER: OPT_NOT_SUPPORTED,
|
|
20
24
|
DELAY: OPT_NOT_SUPPORTED,
|
|
@@ -36,6 +40,7 @@ class gau(HttpCrawler):
|
|
|
36
40
|
TIMEOUT: 'timeout',
|
|
37
41
|
USER_AGENT: OPT_NOT_SUPPORTED,
|
|
38
42
|
}
|
|
43
|
+
item_loaders = [JSONSerializer()]
|
|
39
44
|
install_cmd = 'go install -v github.com/lc/gau/v2/cmd/gau@latest'
|
|
40
45
|
install_github_handle = 'lc/gau'
|
|
41
46
|
proxychains = False
|
secator/tasks/gf.py
CHANGED
|
@@ -12,7 +12,7 @@ class gf(Tagger):
|
|
|
12
12
|
input_flag = OPT_PIPE_INPUT
|
|
13
13
|
version_flag = OPT_NOT_SUPPORTED
|
|
14
14
|
opts = {
|
|
15
|
-
'pattern': {'type': str, 'help': 'Pattern names to match against (comma-delimited)'}
|
|
15
|
+
'pattern': {'type': str, 'help': 'Pattern names to match against (comma-delimited)', 'required': True}
|
|
16
16
|
}
|
|
17
17
|
opt_key_map = {
|
|
18
18
|
'pattern': ''
|
|
@@ -26,7 +26,7 @@ class gf(Tagger):
|
|
|
26
26
|
|
|
27
27
|
@staticmethod
|
|
28
28
|
def item_loader(self, line):
|
|
29
|
-
|
|
29
|
+
yield {'match': line, 'name': self.get_opt_value('pattern').rstrip() + ' pattern'} # noqa: E731,E501
|
|
30
30
|
|
|
31
31
|
@staticmethod
|
|
32
32
|
def on_item(self, item):
|
secator/tasks/gospider.py
CHANGED
|
@@ -8,6 +8,7 @@ from secator.definitions import (CONTENT_LENGTH, DELAY, DEPTH, FILTER_CODES,
|
|
|
8
8
|
OPT_NOT_SUPPORTED, PROXY, RATE_LIMIT, RETRIES,
|
|
9
9
|
STATUS_CODE, THREADS, TIMEOUT, URL, USER_AGENT)
|
|
10
10
|
from secator.output_types import Url
|
|
11
|
+
from secator.serializers import JSONSerializer
|
|
11
12
|
from secator.tasks._categories import HttpCrawler
|
|
12
13
|
|
|
13
14
|
|
|
@@ -44,6 +45,7 @@ class gospider(HttpCrawler):
|
|
|
44
45
|
FOLLOW_REDIRECT: lambda x: not x,
|
|
45
46
|
DELAY: lambda x: round(x) if isinstance(x, float) else x
|
|
46
47
|
}
|
|
48
|
+
item_loaders = [JSONSerializer()]
|
|
47
49
|
output_map = {
|
|
48
50
|
Url: {
|
|
49
51
|
URL: 'output',
|
|
@@ -62,6 +64,8 @@ class gospider(HttpCrawler):
|
|
|
62
64
|
@staticmethod
|
|
63
65
|
def validate_item(self, item):
|
|
64
66
|
"""Keep only items that match the same host."""
|
|
67
|
+
if not isinstance(item, dict):
|
|
68
|
+
return False
|
|
65
69
|
try:
|
|
66
70
|
netloc_in = furl(item['input']).netloc
|
|
67
71
|
netloc_out = furl(item['output']).netloc
|
secator/tasks/grype.py
CHANGED
|
@@ -35,25 +35,25 @@ class grype(VulnCode):
|
|
|
35
35
|
@staticmethod
|
|
36
36
|
def item_loader(self, line):
|
|
37
37
|
"""Load vulnerabilty dicts from grype line output."""
|
|
38
|
-
split = [i for i in line.split('
|
|
39
|
-
if
|
|
40
|
-
return
|
|
41
|
-
|
|
38
|
+
split = [i for i in line.split(' ') if i]
|
|
39
|
+
if len(split) not in [5, 6] or split[0] == 'NAME':
|
|
40
|
+
return
|
|
41
|
+
versions_fixed = None
|
|
42
42
|
if len(split) == 5: # no version fixed
|
|
43
43
|
product, version, product_type, vuln_id, severity = tuple(split)
|
|
44
44
|
elif len(split) == 6:
|
|
45
|
-
product, version,
|
|
45
|
+
product, version, versions_fixed, product_type, vuln_id, severity = tuple(split)
|
|
46
46
|
extra_data = {
|
|
47
47
|
'lang': product_type,
|
|
48
48
|
'product': product,
|
|
49
49
|
'version': version,
|
|
50
50
|
}
|
|
51
|
-
if
|
|
52
|
-
extra_data['
|
|
51
|
+
if versions_fixed:
|
|
52
|
+
extra_data['versions_fixed'] = [c.strip() for c in versions_fixed.split(', ')]
|
|
53
53
|
data = {
|
|
54
54
|
'id': vuln_id,
|
|
55
55
|
'name': vuln_id,
|
|
56
|
-
'matched_at': self.
|
|
56
|
+
'matched_at': self.inputs[0],
|
|
57
57
|
'confidence': 'medium',
|
|
58
58
|
'severity': severity.lower(),
|
|
59
59
|
'provider': 'grype',
|
|
@@ -76,4 +76,4 @@ class grype(VulnCode):
|
|
|
76
76
|
data.update(vuln)
|
|
77
77
|
data['severity'] = data['severity'] or severity.lower()
|
|
78
78
|
data['extra_data'] = extra_data
|
|
79
|
-
|
|
79
|
+
yield data
|
secator/tasks/h8mail.py
CHANGED
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
from secator.decorators import task
|
|
5
5
|
from secator.definitions import EMAIL, OUTPUT_PATH
|
|
6
6
|
from secator.tasks._categories import OSInt
|
|
7
|
-
from secator.output_types import UserAccount
|
|
7
|
+
from secator.output_types import UserAccount, Info, Error
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@task()
|
|
@@ -17,16 +17,10 @@ class h8mail(OSInt):
|
|
|
17
17
|
file_flag = '-domain'
|
|
18
18
|
version_flag = '--help'
|
|
19
19
|
opt_prefix = '--'
|
|
20
|
-
opt_key_map = {
|
|
21
|
-
|
|
22
|
-
}
|
|
23
20
|
opts = {
|
|
24
21
|
'config': {'type': str, 'help': 'Configuration file for API keys'},
|
|
25
22
|
'local_breach': {'type': str, 'short': 'lb', 'help': 'Local breach file'}
|
|
26
23
|
}
|
|
27
|
-
output_map = {
|
|
28
|
-
}
|
|
29
|
-
|
|
30
24
|
install_cmd = 'pipx install h8mail'
|
|
31
25
|
|
|
32
26
|
@staticmethod
|
|
@@ -37,44 +31,40 @@ class h8mail(OSInt):
|
|
|
37
31
|
self.output_path = output_path
|
|
38
32
|
self.cmd = self.cmd.replace('--json', f'--json {self.output_path}')
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
self.
|
|
43
|
-
|
|
44
|
-
if self.return_code != 0:
|
|
34
|
+
@staticmethod
|
|
35
|
+
def on_cmd_done(self):
|
|
36
|
+
if not os.path.exists(self.output_path):
|
|
37
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
45
38
|
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
for entry in entries:
|
|
63
|
-
source, site_name = tuple(entry.split(':'))
|
|
64
|
-
yield UserAccount(**{
|
|
65
|
-
"site_name": site_name,
|
|
66
|
-
"username": email.split('@')[0],
|
|
67
|
-
"email": email,
|
|
68
|
-
"extra_data": {
|
|
69
|
-
'source': source
|
|
70
|
-
},
|
|
71
|
-
})
|
|
72
|
-
else:
|
|
39
|
+
|
|
40
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
41
|
+
with open(self.output_path, 'r') as f:
|
|
42
|
+
data = json.load(f)
|
|
43
|
+
|
|
44
|
+
targets = data['targets']
|
|
45
|
+
for target in targets:
|
|
46
|
+
email = target['target']
|
|
47
|
+
target_data = target.get('data', [])
|
|
48
|
+
pwn_num = target['pwn_num']
|
|
49
|
+
if not pwn_num > 0:
|
|
50
|
+
continue
|
|
51
|
+
if len(target_data) > 0:
|
|
52
|
+
entries = target_data[0]
|
|
53
|
+
for entry in entries:
|
|
54
|
+
source, site_name = tuple(entry.split(':'))
|
|
73
55
|
yield UserAccount(**{
|
|
56
|
+
"site_name": site_name,
|
|
74
57
|
"username": email.split('@')[0],
|
|
75
58
|
"email": email,
|
|
76
59
|
"extra_data": {
|
|
77
|
-
'source':
|
|
60
|
+
'source': source
|
|
78
61
|
},
|
|
79
62
|
})
|
|
80
|
-
|
|
63
|
+
else:
|
|
64
|
+
yield UserAccount(**{
|
|
65
|
+
"username": email.split('@')[0],
|
|
66
|
+
"email": email,
|
|
67
|
+
"extra_data": {
|
|
68
|
+
'source': self.get_opt_value('local_breach')
|
|
69
|
+
},
|
|
70
|
+
})
|
secator/tasks/httpx.py
CHANGED
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
3
|
from secator.decorators import task
|
|
4
|
-
from secator.definitions import (
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
MATCH_CODES, MATCH_REGEX, MATCH_SIZE,
|
|
8
|
-
MATCH_WORDS, METHOD, OPT_NOT_SUPPORTED, PROXY,
|
|
9
|
-
RATE_LIMIT, RETRIES, THREADS,
|
|
10
|
-
TIMEOUT, URL, USER_AGENT)
|
|
4
|
+
from secator.definitions import (DELAY, DEPTH, FILTER_CODES, FILTER_REGEX, FILTER_SIZE, FILTER_WORDS, FOLLOW_REDIRECT,
|
|
5
|
+
HEADER, MATCH_CODES, MATCH_REGEX, MATCH_SIZE, MATCH_WORDS, METHOD, OPT_NOT_SUPPORTED,
|
|
6
|
+
PROXY, RATE_LIMIT, RETRIES, THREADS, TIMEOUT, URL, USER_AGENT)
|
|
11
7
|
from secator.config import CONFIG
|
|
8
|
+
from secator.output_types import Url, Subdomain
|
|
9
|
+
from secator.serializers import JSONSerializer
|
|
12
10
|
from secator.tasks._categories import Http
|
|
13
|
-
from secator.utils import sanitize_url
|
|
11
|
+
from secator.utils import (sanitize_url, extract_domain_info, extract_subdomains_from_fqdn)
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
@task()
|
|
17
15
|
class httpx(Http):
|
|
18
16
|
"""Fast and multi-purpose HTTP toolkit."""
|
|
19
|
-
cmd =
|
|
17
|
+
cmd = 'httpx'
|
|
20
18
|
file_flag = '-l'
|
|
21
19
|
input_flag = '-u'
|
|
22
20
|
json_flag = '-json'
|
|
23
21
|
opts = {
|
|
24
22
|
# 'silent': {'is_flag': True, 'default': False, 'help': 'Silent mode'},
|
|
25
|
-
# 'td': {'is_flag': True, 'default': True, 'help': 'Tech detection'},
|
|
26
23
|
# 'irr': {'is_flag': True, 'default': False, 'help': 'Include http request / response'},
|
|
27
24
|
'fep': {'is_flag': True, 'default': False, 'help': 'Error Page Classifier and Filtering'},
|
|
28
25
|
'favicon': {'is_flag': True, 'default': False, 'help': 'Favicon hash'},
|
|
@@ -35,6 +32,11 @@ class httpx(Http):
|
|
|
35
32
|
'screenshot': {'is_flag': True, 'short': 'ss', 'default': False, 'help': 'Screenshot response'},
|
|
36
33
|
'system_chrome': {'is_flag': True, 'default': False, 'help': 'Use local installed Chrome for screenshot'},
|
|
37
34
|
'headless_options': {'is_flag': False, 'short': 'ho', 'default': None, 'help': 'Headless Chrome additional options'},
|
|
35
|
+
'follow_host_redirects': {'is_flag': True, 'short': 'fhr', 'default': None, 'help': 'Follow redirects on the same host'}, # noqa: E501
|
|
36
|
+
'tech_detect': {'is_flag': True, 'short': 'td', 'default': True, 'help': 'Tech detection'},
|
|
37
|
+
'tls_grab': {'is_flag': True, 'short': 'tlsg', 'default': False, 'help': 'Grab some informations from the tls certificate'}, # noqa: E501
|
|
38
|
+
'rstr': {'type': int, 'default': CONFIG.http.response_max_size_bytes, 'help': 'Max body size to read (bytes)'},
|
|
39
|
+
'rsts': {'type': int, 'default': CONFIG.http.response_max_size_bytes, 'help': 'Max body size to save (bytes)'}
|
|
38
40
|
}
|
|
39
41
|
opt_key_map = {
|
|
40
42
|
HEADER: 'header',
|
|
@@ -61,6 +63,8 @@ class httpx(Http):
|
|
|
61
63
|
opt_value_map = {
|
|
62
64
|
DELAY: lambda x: str(x) + 's' if x else None,
|
|
63
65
|
}
|
|
66
|
+
item_loaders = [JSONSerializer()]
|
|
67
|
+
output_types = [Url, Subdomain]
|
|
64
68
|
install_cmd = 'go install -v github.com/projectdiscovery/httpx/cmd/httpx@latest'
|
|
65
69
|
install_github_handle = 'projectdiscovery/httpx'
|
|
66
70
|
proxychains = False
|
|
@@ -79,19 +83,23 @@ class httpx(Http):
|
|
|
79
83
|
self.cmd += f' -srd {self.reports_folder}/.outputs'
|
|
80
84
|
if screenshot:
|
|
81
85
|
self.cmd += ' -esb -ehb'
|
|
86
|
+
self.domains = []
|
|
82
87
|
|
|
83
88
|
@staticmethod
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
def on_json_loaded(self, item):
|
|
90
|
+
item = self._preprocess_url(item)
|
|
91
|
+
yield item
|
|
92
|
+
tls = item.get('tls', None)
|
|
93
|
+
if tls:
|
|
94
|
+
subject_cn = tls.get('subject_cn', None)
|
|
95
|
+
subject_an = tls.get('subject_an', [])
|
|
96
|
+
cert_domains = subject_an
|
|
97
|
+
if subject_cn:
|
|
98
|
+
cert_domains.append(subject_cn)
|
|
99
|
+
for cert_domain in cert_domains:
|
|
100
|
+
subdomain = self._create_subdomain_from_tls_cert(cert_domain, item['url'])
|
|
101
|
+
if subdomain:
|
|
102
|
+
yield subdomain
|
|
95
103
|
|
|
96
104
|
@staticmethod
|
|
97
105
|
def on_end(self):
|
|
@@ -107,3 +115,32 @@ class httpx(Http):
|
|
|
107
115
|
os.remove(index_spath)
|
|
108
116
|
if os.path.exists(index_spath2):
|
|
109
117
|
os.remove(index_spath2)
|
|
118
|
+
|
|
119
|
+
def _preprocess_url(self, item):
|
|
120
|
+
"""Replace time string by float, sanitize URL, get final redirect URL."""
|
|
121
|
+
for k, v in item.items():
|
|
122
|
+
if k == 'time':
|
|
123
|
+
response_time = float(''.join(ch for ch in v if not ch.isalpha()))
|
|
124
|
+
if v[-2:] == 'ms':
|
|
125
|
+
response_time = response_time / 1000
|
|
126
|
+
item[k] = response_time
|
|
127
|
+
elif k == URL:
|
|
128
|
+
item[k] = sanitize_url(v)
|
|
129
|
+
item[URL] = item.get('final_url') or item[URL]
|
|
130
|
+
return item
|
|
131
|
+
|
|
132
|
+
def _create_subdomain_from_tls_cert(self, domain, url):
|
|
133
|
+
"""Extract subdomains from TLS certificate."""
|
|
134
|
+
if domain.startswith('*.'):
|
|
135
|
+
domain = domain.lstrip('*.')
|
|
136
|
+
if domain in self.domains:
|
|
137
|
+
return None
|
|
138
|
+
url_domain = extract_domain_info(url)
|
|
139
|
+
url_domains = extract_subdomains_from_fqdn(url_domain.fqdn, url_domain.domain, url_domain.suffix)
|
|
140
|
+
if not url_domain or domain not in url_domains:
|
|
141
|
+
return None
|
|
142
|
+
self.domains.append(domain)
|
|
143
|
+
return Subdomain(
|
|
144
|
+
host=domain,
|
|
145
|
+
domain=extract_domain_info(domain, domain_only=True)
|
|
146
|
+
)
|
secator/tasks/katana.py
CHANGED
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import json
|
|
3
2
|
from urllib.parse import urlparse
|
|
4
3
|
|
|
5
4
|
from secator.decorators import task
|
|
6
|
-
from secator.definitions import (CONTENT_TYPE,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
MATCH_WORDS, METHOD, OPT_NOT_SUPPORTED, PROXY,
|
|
12
|
-
RATE_LIMIT, RETRIES, STATUS_CODE,
|
|
13
|
-
STORED_RESPONSE_PATH, TECH,
|
|
14
|
-
THREADS, TIME, TIMEOUT, URL, USER_AGENT, WEBSERVER, CONTENT_LENGTH)
|
|
5
|
+
from secator.definitions import (CONTENT_TYPE, DELAY, DEPTH, FILTER_CODES, FILTER_REGEX, FILTER_SIZE, FILTER_WORDS,
|
|
6
|
+
FOLLOW_REDIRECT, HEADER, HOST, MATCH_CODES, MATCH_REGEX, MATCH_SIZE, MATCH_WORDS,
|
|
7
|
+
METHOD, OPT_NOT_SUPPORTED, PROXY, RATE_LIMIT, RETRIES, STATUS_CODE,
|
|
8
|
+
STORED_RESPONSE_PATH, TECH, THREADS, TIME, TIMEOUT, URL, USER_AGENT, WEBSERVER,
|
|
9
|
+
CONTENT_LENGTH)
|
|
15
10
|
from secator.config import CONFIG
|
|
16
11
|
from secator.output_types import Url, Tag
|
|
12
|
+
from secator.serializers import JSONSerializer
|
|
17
13
|
from secator.tasks._categories import HttpCrawler
|
|
18
14
|
|
|
19
15
|
|
|
20
16
|
@task()
|
|
21
17
|
class katana(HttpCrawler):
|
|
22
18
|
"""Next-generation crawling and spidering framework."""
|
|
23
|
-
|
|
24
|
-
# TODO: add -jsluice for JS parsing
|
|
25
|
-
cmd = f'katana {DEFAULT_KATANA_FLAGS}'
|
|
19
|
+
cmd = 'katana'
|
|
26
20
|
file_flag = '-list'
|
|
27
21
|
input_flag = '-u'
|
|
28
22
|
json_flag = '-jsonl'
|
|
@@ -30,7 +24,13 @@ class katana(HttpCrawler):
|
|
|
30
24
|
'headless': {'is_flag': True, 'short': 'hl', 'help': 'Headless mode'},
|
|
31
25
|
'system_chrome': {'is_flag': True, 'short': 'sc', 'help': 'Use local installed chrome browser'},
|
|
32
26
|
'form_extraction': {'is_flag': True, 'short': 'fx', 'help': 'Detect forms'},
|
|
33
|
-
'store_responses': {'is_flag': True, 'short': 'sr', 'default': CONFIG.http.store_responses, 'help': 'Store responses'}
|
|
27
|
+
'store_responses': {'is_flag': True, 'short': 'sr', 'default': CONFIG.http.store_responses, 'help': 'Store responses'}, # noqa: E501
|
|
28
|
+
'form_fill': {'is_flag': True, 'short': 'ff', 'help': 'Enable form filling'},
|
|
29
|
+
'js_crawl': {'is_flag': True, 'short': 'jc', 'default': True, 'help': 'Enable endpoint parsing / crawling in javascript file'}, # noqa: E501
|
|
30
|
+
'jsluice': {'is_flag': True, 'short': 'jsl', 'default': True, 'help': 'Enable jsluice parsing in javascript file (memory intensive)'}, # noqa: E501
|
|
31
|
+
'known_files': {'type': str, 'short': 'kf', 'default': 'all', 'help': 'Enable crawling of known files (all, robotstxt, sitemapxml)'}, # noqa: E501
|
|
32
|
+
'omit_raw': {'is_flag': True, 'short': 'or', 'default': True, 'help': 'Omit raw requests/responses from jsonl output'}, # noqa: E501
|
|
33
|
+
'omit_body': {'is_flag': True, 'short': 'ob', 'default': True, 'help': 'Omit response body from jsonl output'}
|
|
34
34
|
}
|
|
35
35
|
opt_key_map = {
|
|
36
36
|
HEADER: 'headers',
|
|
@@ -52,11 +52,13 @@ class katana(HttpCrawler):
|
|
|
52
52
|
THREADS: 'concurrency',
|
|
53
53
|
TIMEOUT: 'timeout',
|
|
54
54
|
USER_AGENT: OPT_NOT_SUPPORTED,
|
|
55
|
-
'store_responses': 'sr'
|
|
55
|
+
'store_responses': 'sr',
|
|
56
|
+
'form_fill': 'aff'
|
|
56
57
|
}
|
|
57
58
|
opt_value_map = {
|
|
58
59
|
DELAY: lambda x: int(x) if isinstance(x, float) else x
|
|
59
60
|
}
|
|
61
|
+
item_loaders = [JSONSerializer()]
|
|
60
62
|
output_map = {
|
|
61
63
|
Url: {
|
|
62
64
|
URL: lambda x: x['request']['endpoint'],
|
|
@@ -72,7 +74,6 @@ class katana(HttpCrawler):
|
|
|
72
74
|
# TAGS: lambda x: x['response'].get('server')
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
|
-
item_loaders = []
|
|
76
77
|
install_cmd = 'sudo apt install build-essential && go install -v github.com/projectdiscovery/katana/cmd/katana@latest'
|
|
77
78
|
install_github_handle = 'projectdiscovery/katana'
|
|
78
79
|
proxychains = False
|
|
@@ -81,12 +82,7 @@ class katana(HttpCrawler):
|
|
|
81
82
|
profile = 'io'
|
|
82
83
|
|
|
83
84
|
@staticmethod
|
|
84
|
-
def
|
|
85
|
-
try:
|
|
86
|
-
item = json.loads(item)
|
|
87
|
-
except json.JSONDecodeError:
|
|
88
|
-
return None
|
|
89
|
-
|
|
85
|
+
def on_json_loaded(self, item):
|
|
90
86
|
# form detection
|
|
91
87
|
forms = item.get('response', {}).get('forms', [])
|
|
92
88
|
if forms:
|
secator/tasks/maigret.py
CHANGED
|
@@ -7,7 +7,7 @@ from secator.decorators import task
|
|
|
7
7
|
from secator.definitions import (DELAY, EXTRA_DATA, OPT_NOT_SUPPORTED, OUTPUT_PATH, PROXY,
|
|
8
8
|
RATE_LIMIT, RETRIES, SITE_NAME, THREADS,
|
|
9
9
|
TIMEOUT, URL, USERNAME)
|
|
10
|
-
from secator.output_types import UserAccount
|
|
10
|
+
from secator.output_types import UserAccount, Info, Error
|
|
11
11
|
from secator.tasks._categories import ReconUser
|
|
12
12
|
|
|
13
13
|
logger = logging.getLogger(__name__)
|
|
@@ -45,34 +45,36 @@ class maigret(ReconUser):
|
|
|
45
45
|
socks5_proxy = True
|
|
46
46
|
profile = 'io'
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
@staticmethod
|
|
49
|
+
def on_init(self):
|
|
50
|
+
self.output_path = self.get_opt_value(OUTPUT_PATH)
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def on_cmd_done(self):
|
|
54
|
+
# Search output path in cmd output
|
|
55
55
|
if not self.output_path:
|
|
56
|
-
|
|
57
|
-
if
|
|
58
|
-
|
|
56
|
+
matches = re.findall('JSON ndjson report for .* saved in (.*)', self.output)
|
|
57
|
+
if not matches:
|
|
58
|
+
yield Error(message='JSON output file not found in command output.')
|
|
59
59
|
return
|
|
60
|
-
self.output_path =
|
|
61
|
-
|
|
62
|
-
if self.
|
|
63
|
-
self.
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
self.output_path = matches
|
|
61
|
+
|
|
62
|
+
if not isinstance(self.output_path, list):
|
|
63
|
+
self.output_path = [self.output_path]
|
|
64
|
+
|
|
65
|
+
for path in self.output_path:
|
|
66
|
+
if not os.path.exists(path):
|
|
67
|
+
yield Error(message=f'Could not find JSON results in {path}')
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
yield Info(message=f'JSON results saved to {path}')
|
|
71
|
+
with open(path, 'r') as f:
|
|
66
72
|
data = [json.loads(line) for line in f.read().splitlines()]
|
|
67
73
|
for item in data:
|
|
68
74
|
yield item
|
|
69
|
-
self.print_item_count = prev
|
|
70
|
-
|
|
71
|
-
@staticmethod
|
|
72
|
-
def on_init(self):
|
|
73
|
-
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
74
|
-
self.output_path = output_path
|
|
75
75
|
|
|
76
76
|
@staticmethod
|
|
77
77
|
def validate_item(self, item):
|
|
78
|
-
|
|
78
|
+
if isinstance(item, dict):
|
|
79
|
+
return item['http_status'] == 200
|
|
80
|
+
return True
|
secator/tasks/mapcidr.py
CHANGED
|
@@ -23,11 +23,10 @@ class mapcidr(ReconIp):
|
|
|
23
23
|
RATE_LIMIT: OPT_NOT_SUPPORTED,
|
|
24
24
|
RETRIES: OPT_NOT_SUPPORTED,
|
|
25
25
|
TIMEOUT: OPT_NOT_SUPPORTED,
|
|
26
|
-
THREADS: OPT_NOT_SUPPORTED,
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
@staticmethod
|
|
30
29
|
def item_loader(self, line):
|
|
31
30
|
if validators.ipv4(line) or validators.ipv6(line):
|
|
32
|
-
|
|
33
|
-
return
|
|
31
|
+
yield {'ip': line, 'alive': False}
|
|
32
|
+
return
|