secator 0.6.0__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.

Files changed (84) hide show
  1. secator/celery.py +160 -185
  2. secator/celery_utils.py +268 -0
  3. secator/cli.py +327 -106
  4. secator/config.py +27 -11
  5. secator/configs/workflows/host_recon.yaml +5 -3
  6. secator/configs/workflows/port_scan.yaml +7 -3
  7. secator/configs/workflows/url_bypass.yaml +10 -0
  8. secator/configs/workflows/url_vuln.yaml +1 -1
  9. secator/decorators.py +169 -92
  10. secator/definitions.py +10 -3
  11. secator/exporters/__init__.py +7 -5
  12. secator/exporters/console.py +10 -0
  13. secator/exporters/csv.py +27 -19
  14. secator/exporters/gdrive.py +16 -11
  15. secator/exporters/json.py +3 -1
  16. secator/exporters/table.py +30 -2
  17. secator/exporters/txt.py +20 -16
  18. secator/hooks/gcs.py +53 -0
  19. secator/hooks/mongodb.py +53 -27
  20. secator/output_types/__init__.py +29 -11
  21. secator/output_types/_base.py +11 -1
  22. secator/output_types/error.py +36 -0
  23. secator/output_types/exploit.py +1 -1
  24. secator/output_types/info.py +24 -0
  25. secator/output_types/ip.py +7 -0
  26. secator/output_types/port.py +8 -1
  27. secator/output_types/progress.py +5 -0
  28. secator/output_types/record.py +3 -1
  29. secator/output_types/stat.py +33 -0
  30. secator/output_types/tag.py +6 -4
  31. secator/output_types/url.py +6 -3
  32. secator/output_types/vulnerability.py +3 -2
  33. secator/output_types/warning.py +24 -0
  34. secator/report.py +55 -23
  35. secator/rich.py +44 -39
  36. secator/runners/_base.py +622 -635
  37. secator/runners/_helpers.py +5 -91
  38. secator/runners/celery.py +18 -0
  39. secator/runners/command.py +364 -211
  40. secator/runners/scan.py +8 -24
  41. secator/runners/task.py +21 -55
  42. secator/runners/workflow.py +41 -40
  43. secator/scans/__init__.py +28 -0
  44. secator/serializers/dataclass.py +6 -0
  45. secator/serializers/json.py +10 -5
  46. secator/serializers/regex.py +12 -4
  47. secator/tasks/_categories.py +5 -2
  48. secator/tasks/bbot.py +293 -0
  49. secator/tasks/bup.py +98 -0
  50. secator/tasks/cariddi.py +38 -49
  51. secator/tasks/dalfox.py +3 -0
  52. secator/tasks/dirsearch.py +12 -23
  53. secator/tasks/dnsx.py +49 -30
  54. secator/tasks/dnsxbrute.py +2 -0
  55. secator/tasks/feroxbuster.py +8 -17
  56. secator/tasks/ffuf.py +3 -2
  57. secator/tasks/fping.py +3 -3
  58. secator/tasks/gau.py +5 -0
  59. secator/tasks/gf.py +2 -2
  60. secator/tasks/gospider.py +4 -0
  61. secator/tasks/grype.py +9 -9
  62. secator/tasks/h8mail.py +31 -41
  63. secator/tasks/httpx.py +58 -21
  64. secator/tasks/katana.py +18 -22
  65. secator/tasks/maigret.py +26 -24
  66. secator/tasks/mapcidr.py +2 -3
  67. secator/tasks/msfconsole.py +4 -16
  68. secator/tasks/naabu.py +3 -1
  69. secator/tasks/nmap.py +50 -35
  70. secator/tasks/nuclei.py +9 -2
  71. secator/tasks/searchsploit.py +17 -9
  72. secator/tasks/subfinder.py +5 -1
  73. secator/tasks/wpscan.py +79 -93
  74. secator/template.py +61 -45
  75. secator/thread.py +24 -0
  76. secator/utils.py +330 -80
  77. secator/utils_test.py +48 -23
  78. secator/workflows/__init__.py +28 -0
  79. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/METADATA +11 -5
  80. secator-0.7.0.dist-info/RECORD +115 -0
  81. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/WHEEL +1 -1
  82. secator-0.6.0.dist-info/RECORD +0 -101
  83. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/entry_points.txt +0 -0
  84. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -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',
@@ -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, OUTPUT_PATH, PROXY,
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.input_path:
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
- return item['type'] == 'response'
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.method = self.get_opt_value(METHOD) or 'GET'
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 {'ip': line, 'alive': True}
38
- return None
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
- return {'match': line, 'name': self.get_opt_value('pattern').rstrip() + ' pattern'} # noqa: E731,E501
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(' ') if i]
39
- if not len(split) in [5, 6] or split[0] == 'NAME':
40
- return None
41
- version_fixed = None
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, version_fixed, product_type, vuln_id, severity = tuple(split)
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 version_fixed:
52
- extra_data['version_fixed'] = version_fixed
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.input,
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
- return data
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
- def yielder(self):
41
- prev = self.print_item_count
42
- self.print_item_count = False
43
- list(super().yielder())
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
- self.results = []
47
- if os.path.exists(self.output_path):
48
- with open(self.output_path, 'r') as f:
49
- data = json.load(f)
50
- if self.orig: # original h8mail output
51
- yield data
52
- return
53
- targets = data['targets']
54
- for target in targets:
55
- email = target['target']
56
- target_data = target.get('data', [])
57
- pwn_num = target['pwn_num']
58
- if not pwn_num > 0:
59
- continue
60
- if len(target_data) > 0:
61
- entries = target_data[0]
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': self.get_opt_value('local_breach')
60
+ 'source': source
78
61
  },
79
62
  })
80
- self.print_item_count = prev
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 (DEFAULT_HTTPX_FLAGS, DELAY, DEPTH,
5
- FILTER_CODES, FILTER_REGEX, FILTER_SIZE,
6
- FILTER_WORDS, FOLLOW_REDIRECT, HEADER,
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 = f'httpx {DEFAULT_HTTPX_FLAGS}'
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 on_item_pre_convert(self, item):
85
- for k, v in item.items():
86
- if k == 'time':
87
- response_time = float(''.join(ch for ch in v if not ch.isalpha()))
88
- if v[-2:] == 'ms':
89
- response_time = response_time / 1000
90
- item[k] = response_time
91
- elif k == URL:
92
- item[k] = sanitize_url(v)
93
- item[URL] = item.get('final_url') or item[URL]
94
- return item
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, DEFAULT_KATANA_FLAGS,
7
- DELAY, DEPTH,
8
- FILTER_CODES, FILTER_REGEX, FILTER_SIZE,
9
- FILTER_WORDS, FOLLOW_REDIRECT, HEADER, HOST,
10
- MATCH_CODES, MATCH_REGEX, MATCH_SIZE,
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
- # TODO: add -fx for form detection and extract 'forms' from the output with custom item_loader
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 item_loader(self, item):
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
- def yielder(self):
49
- prev = self.print_item_count
50
- self.print_item_count = False
51
- yield from super().yielder()
52
- if self.return_code != 0:
53
- return
54
- self.results = []
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
- match = re.search('JSON ndjson report for .* saved in (.*)', self.output)
57
- if match is None:
58
- logger.warning('JSON output file not found in command output.')
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 = match.group(1)
61
- note = f'maigret JSON results saved to {self.output_path}'
62
- if self.print_line:
63
- self._print(note)
64
- if os.path.exists(self.output_path):
65
- with open(self.output_path, 'r') as f:
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
- return item['http_status'] == 200
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
- return {'ip': line, 'alive': False}
33
- return None
31
+ yield {'ip': line, 'alive': False}
32
+ return