secator 0.15.1__py3-none-any.whl → 0.16.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 (106) hide show
  1. secator/celery.py +40 -24
  2. secator/celery_signals.py +71 -68
  3. secator/celery_utils.py +43 -27
  4. secator/cli.py +520 -280
  5. secator/cli_helper.py +394 -0
  6. secator/click.py +87 -0
  7. secator/config.py +67 -39
  8. secator/configs/profiles/http_headless.yaml +6 -0
  9. secator/configs/profiles/http_record.yaml +6 -0
  10. secator/configs/profiles/tor.yaml +1 -1
  11. secator/configs/scans/domain.yaml +4 -2
  12. secator/configs/scans/host.yaml +1 -1
  13. secator/configs/scans/network.yaml +1 -4
  14. secator/configs/scans/subdomain.yaml +13 -1
  15. secator/configs/scans/url.yaml +1 -2
  16. secator/configs/workflows/cidr_recon.yaml +6 -4
  17. secator/configs/workflows/code_scan.yaml +1 -1
  18. secator/configs/workflows/host_recon.yaml +29 -3
  19. secator/configs/workflows/subdomain_recon.yaml +67 -16
  20. secator/configs/workflows/url_crawl.yaml +44 -15
  21. secator/configs/workflows/url_dirsearch.yaml +4 -4
  22. secator/configs/workflows/url_fuzz.yaml +25 -17
  23. secator/configs/workflows/url_params_fuzz.yaml +7 -0
  24. secator/configs/workflows/url_vuln.yaml +33 -8
  25. secator/configs/workflows/user_hunt.yaml +2 -1
  26. secator/configs/workflows/wordpress.yaml +5 -3
  27. secator/cve.py +718 -0
  28. secator/decorators.py +0 -454
  29. secator/definitions.py +49 -30
  30. secator/exporters/_base.py +2 -2
  31. secator/exporters/console.py +2 -2
  32. secator/exporters/table.py +4 -3
  33. secator/exporters/txt.py +1 -1
  34. secator/hooks/mongodb.py +2 -4
  35. secator/installer.py +77 -49
  36. secator/loader.py +116 -0
  37. secator/output_types/_base.py +3 -0
  38. secator/output_types/certificate.py +63 -63
  39. secator/output_types/error.py +4 -5
  40. secator/output_types/info.py +2 -2
  41. secator/output_types/ip.py +3 -1
  42. secator/output_types/progress.py +5 -9
  43. secator/output_types/state.py +17 -17
  44. secator/output_types/tag.py +3 -0
  45. secator/output_types/target.py +10 -2
  46. secator/output_types/url.py +19 -7
  47. secator/output_types/vulnerability.py +11 -7
  48. secator/output_types/warning.py +2 -2
  49. secator/report.py +27 -15
  50. secator/rich.py +18 -10
  51. secator/runners/_base.py +446 -233
  52. secator/runners/_helpers.py +133 -24
  53. secator/runners/command.py +182 -102
  54. secator/runners/scan.py +33 -5
  55. secator/runners/task.py +13 -7
  56. secator/runners/workflow.py +105 -72
  57. secator/scans/__init__.py +2 -2
  58. secator/serializers/dataclass.py +20 -20
  59. secator/tasks/__init__.py +4 -4
  60. secator/tasks/_categories.py +39 -27
  61. secator/tasks/arjun.py +9 -5
  62. secator/tasks/bbot.py +53 -21
  63. secator/tasks/bup.py +19 -5
  64. secator/tasks/cariddi.py +24 -3
  65. secator/tasks/dalfox.py +26 -7
  66. secator/tasks/dirsearch.py +10 -4
  67. secator/tasks/dnsx.py +70 -25
  68. secator/tasks/feroxbuster.py +11 -3
  69. secator/tasks/ffuf.py +42 -6
  70. secator/tasks/fping.py +20 -8
  71. secator/tasks/gau.py +3 -1
  72. secator/tasks/gf.py +3 -3
  73. secator/tasks/gitleaks.py +2 -2
  74. secator/tasks/gospider.py +7 -1
  75. secator/tasks/grype.py +5 -4
  76. secator/tasks/h8mail.py +2 -1
  77. secator/tasks/httpx.py +18 -5
  78. secator/tasks/katana.py +35 -15
  79. secator/tasks/maigret.py +4 -4
  80. secator/tasks/mapcidr.py +3 -3
  81. secator/tasks/msfconsole.py +4 -4
  82. secator/tasks/naabu.py +2 -2
  83. secator/tasks/nmap.py +12 -14
  84. secator/tasks/nuclei.py +3 -3
  85. secator/tasks/searchsploit.py +4 -5
  86. secator/tasks/subfinder.py +2 -2
  87. secator/tasks/testssl.py +264 -263
  88. secator/tasks/trivy.py +5 -5
  89. secator/tasks/wafw00f.py +21 -3
  90. secator/tasks/wpprobe.py +90 -83
  91. secator/tasks/wpscan.py +6 -5
  92. secator/template.py +218 -104
  93. secator/thread.py +15 -15
  94. secator/tree.py +196 -0
  95. secator/utils.py +131 -123
  96. secator/utils_test.py +60 -19
  97. secator/workflows/__init__.py +2 -2
  98. {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/METADATA +36 -36
  99. secator-0.16.0.dist-info/RECORD +132 -0
  100. secator/configs/profiles/default.yaml +0 -8
  101. secator/configs/workflows/url_nuclei.yaml +0 -11
  102. secator/tasks/dnsxbrute.py +0 -42
  103. secator-0.15.1.dist-info/RECORD +0 -128
  104. {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/WHEEL +0 -0
  105. {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/entry_points.txt +0 -0
  106. {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,12 +2,13 @@ import time
2
2
  from dataclasses import dataclass, field
3
3
 
4
4
  from secator.output_types import OutputType
5
- from secator.utils import rich_to_ansi, rich_escape as _s
5
+ from secator.utils import autodetect_type, rich_to_ansi, rich_escape as _s
6
6
 
7
7
 
8
8
  @dataclass
9
9
  class Target(OutputType):
10
10
  name: str
11
+ type: str = ''
11
12
  _source: str = field(default='', repr=True)
12
13
  _type: str = field(default='target', repr=True)
13
14
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
@@ -19,12 +20,19 @@ class Target(OutputType):
19
20
 
20
21
  _table_fields = [
21
22
  'name',
23
+ 'type',
22
24
  ]
23
- _sort_by = ('name',)
25
+ _sort_by = ('type', 'name')
26
+
27
+ def __post_init__(self):
28
+ if not self.type:
29
+ self.type = autodetect_type(self.name)
24
30
 
25
31
  def __str__(self):
26
32
  return self.name
27
33
 
28
34
  def __repr__(self):
29
35
  s = f'🎯 {_s(self.name)}'
36
+ if self.type:
37
+ s += f' ({self.type})'
30
38
  return rich_to_ansi(s)
@@ -2,9 +2,9 @@ import time
2
2
  from dataclasses import dataclass, field
3
3
 
4
4
  from secator.definitions import (CONTENT_LENGTH, CONTENT_TYPE, STATUS_CODE,
5
- TECH, TIME, TITLE, URL, WEBSERVER)
5
+ TECH, TITLE, URL, WEBSERVER, METHOD)
6
6
  from secator.output_types import OutputType
7
- from secator.utils import rich_to_ansi, trim_string, rich_escape as _s
7
+ from secator.utils import rich_to_ansi, trim_string, format_object, rich_escape as _s
8
8
  from secator.config import CONFIG
9
9
 
10
10
 
@@ -24,7 +24,9 @@ class Url(OutputType):
24
24
  lines: int = field(default=0, compare=False)
25
25
  screenshot_path: str = field(default='', compare=False)
26
26
  stored_response_path: str = field(default='', compare=False)
27
- headers: dict = field(default_factory=dict, repr=True, compare=False)
27
+ response_headers: dict = field(default_factory=dict, repr=True, compare=False)
28
+ request_headers: dict = field(default_factory=dict, repr=True, compare=False)
29
+ extra_data: dict = field(default_factory=dict, compare=False)
28
30
  _source: str = field(default='', repr=True, compare=False)
29
31
  _type: str = field(default='url', repr=True)
30
32
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
@@ -36,13 +38,15 @@ class Url(OutputType):
36
38
 
37
39
  _table_fields = [
38
40
  URL,
41
+ METHOD,
39
42
  STATUS_CODE,
40
43
  TITLE,
41
44
  WEBSERVER,
42
45
  TECH,
43
46
  CONTENT_TYPE,
44
47
  CONTENT_LENGTH,
45
- TIME
48
+ 'stored_response_path',
49
+ 'screenshot_path',
46
50
  ]
47
51
  _sort_by = (URL,)
48
52
 
@@ -59,15 +63,17 @@ class Url(OutputType):
59
63
  s = f'🔗 [white]{_s(self.url)}'
60
64
  if self.method and self.method != 'GET':
61
65
  s += rf' \[[turquoise4]{self.method}[/]]'
66
+ if self.request_headers:
67
+ s += rf'{format_object(self.request_headers, "gold3", skip_keys=["user_agent"])}'
62
68
  if self.status_code and self.status_code != 0:
63
69
  if self.status_code < 400:
64
70
  s += rf' \[[green]{self.status_code}[/]]'
65
71
  else:
66
72
  s += rf' \[[red]{self.status_code}[/]]'
67
73
  if self.title:
68
- s += rf' \[[green]{trim_string(self.title)}[/]]'
74
+ s += rf' \[[spring_green3]{trim_string(self.title)}[/]]'
69
75
  if self.webserver:
70
- s += rf' \[[magenta]{_s(self.webserver)}[/]]'
76
+ s += rf' \[[bold magenta]{_s(self.webserver)}[/]]'
71
77
  if self.tech:
72
78
  techs_str = ', '.join([f'[magenta]{_s(tech)}[/]' for tech in self.tech])
73
79
  s += f' [{techs_str}]'
@@ -77,6 +83,12 @@ class Url(OutputType):
77
83
  cl = str(self.content_length)
78
84
  cl += '[bold red]+[/]' if self.content_length == CONFIG.http.response_max_size_bytes else ''
79
85
  s += rf' \[[magenta]{cl}[/]]'
86
+ if self.response_headers and CONFIG.cli.show_http_response_headers:
87
+ s += rf'{format_object(self.response_headers, "magenta", skip_keys=CONFIG.cli.exclude_http_response_headers)}' # noqa: E501
88
+ if self.extra_data:
89
+ s += format_object(self.extra_data, 'yellow')
80
90
  if self.screenshot_path:
81
- s += rf' \[[magenta]{_s(self.screenshot_path)}[/]]'
91
+ s += rf' [link=file://{self.screenshot_path}]:camera:[/]'
92
+ if self.stored_response_path:
93
+ s += rf' [link=file://{self.stored_response_path}]:pencil:[/]'
82
94
  return rich_to_ansi(s)
@@ -5,7 +5,7 @@ from typing import List
5
5
  from secator.definitions import (CONFIDENCE, CVSS_SCORE, EXTRA_DATA, ID,
6
6
  MATCHED_AT, NAME, REFERENCE, SEVERITY, TAGS)
7
7
  from secator.output_types import OutputType
8
- from secator.utils import rich_to_ansi, rich_escape as _s
8
+ from secator.utils import rich_to_ansi, rich_escape as _s, format_object
9
9
 
10
10
 
11
11
  @dataclass
@@ -66,11 +66,11 @@ class Vulnerability(OutputType):
66
66
 
67
67
  def __repr__(self):
68
68
  data = self.extra_data
69
+
70
+ # TODO: review this
69
71
  if 'data' in data and isinstance(data['data'], list):
70
- data = ','.join(data['data'])
71
- elif isinstance(data, dict):
72
- data = ', '.join([f'{k}:{v}' for k, v in data.items()])
73
- data = _s(data)
72
+ data = data['data']
73
+
74
74
  tags = self.tags
75
75
  colors = {
76
76
  'critical': 'bold red',
@@ -81,12 +81,16 @@ class Vulnerability(OutputType):
81
81
  'unknown': 'dim magenta'
82
82
  }
83
83
  c = colors.get(self.severity, 'dim magenta')
84
- s = rf'🚨 \[[green]{_s(self.name)} [link={_s(self.reference)}]🡕[/link][/]] \[[{c}]{self.severity}[/]] {_s(self.matched_at)}' # noqa: E501
84
+ name = self.name
85
+ if self.reference:
86
+ name += rf' [link={_s(self.reference)}]🡕[/link]'
87
+ s = rf'🚨 \[[green]{name}[/]]'
88
+ s += rf' \[[{c}]{self.severity}[/]] {_s(self.matched_at)}' # noqa: E501
85
89
  if tags:
86
90
  tags_str = ','.join(tags)
87
91
  s += rf' \[[cyan]{_s(tags_str)}[/]]'
88
92
  if data:
89
- s += rf' \[[yellow]{str(data)}[/]]'
93
+ s += format_object(data, 'yellow')
90
94
  if self.confidence == 'low':
91
95
  s = f'[dim]{s}[/]'
92
96
  return rich_to_ansi(s)
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass, field
2
2
  import time
3
3
  from secator.output_types import OutputType
4
- from secator.utils import rich_to_ansi, rich_escape as _s
4
+ from secator.utils import rich_to_ansi
5
5
 
6
6
 
7
7
  @dataclass
@@ -20,5 +20,5 @@ class Warning(OutputType):
20
20
  _sort_by = ('_timestamp',)
21
21
 
22
22
  def __repr__(self):
23
- s = rf"\[[yellow]WRN[/]] {_s(self.message)}"
23
+ s = rf"\[[yellow]WRN[/]] {self.message}"
24
24
  return rich_to_ansi(s)
secator/report.py CHANGED
@@ -2,7 +2,7 @@ import operator
2
2
 
3
3
  from secator.config import CONFIG
4
4
  from secator.output_types import FINDING_TYPES, OutputType
5
- from secator.utils import merge_opts, get_file_timestamp, traceback_as_string
5
+ from secator.utils import get_file_timestamp, traceback_as_string
6
6
  from secator.rich import console
7
7
  from secator.runners._helpers import extract_from_results
8
8
 
@@ -56,14 +56,7 @@ class Report:
56
56
  )
57
57
 
58
58
  def build(self, extractors=[], dedupe=CONFIG.runners.remove_duplicates):
59
- # Trim options
60
- from secator.decorators import DEFAULT_CLI_OPTIONS
61
- opts = merge_opts(self.runner.config.options, self.runner.run_opts)
62
- opts = {
63
- k: v for k, v in opts.items()
64
- if k not in DEFAULT_CLI_OPTIONS and k not in self.runner.print_opts
65
- and v is not None
66
- }
59
+ # Prepare report structure
67
60
  runner_fields = {
68
61
  'name',
69
62
  'status',
@@ -75,8 +68,6 @@ class Report:
75
68
  'run_opts',
76
69
  'results_count'
77
70
  }
78
-
79
- # Prepare report structure
80
71
  data = {
81
72
  'info': {k: v for k, v in self.runner.toDict().items() if k in runner_fields},
82
73
  'results': {}
@@ -96,12 +87,33 @@ class Report:
96
87
  ]
97
88
  if items:
98
89
  if sort_by and all(sort_by):
99
- items = sorted(items, key=operator.attrgetter(*sort_by))
90
+ try:
91
+ items = sorted(items, key=operator.attrgetter(*sort_by))
92
+ except TypeError as e:
93
+ console.print(f'[bold red]Could not sort {output_name} by {sort_by}: {str(e)}[/]')
94
+ console.print(f'[dim]{traceback_as_string(e)}[/]')
100
95
  if dedupe:
101
96
  items = remove_duplicates(items)
102
- # items = [item for item in items if not item._duplicate and item not in dedupe_from]
103
- for extractor in extractors:
104
- items = extract_from_results(items, extractors=[extractor])
97
+ if extractors:
98
+ all_res = []
99
+ extractors_type = [extractor for extractor in extractors if extractor.get('type') == output_name]
100
+ for extractor in extractors_type:
101
+ op = extractor.get('op', 'or')
102
+ res, errors = extract_from_results(items, extractors=[extractor])
103
+ # console.print(f'{extractor} --> {len(res)} results')
104
+ if not res:
105
+ continue
106
+ if errors:
107
+ data['info']['errors'] = errors
108
+ if res:
109
+ if op == 'or':
110
+ all_res = all_res + res
111
+ else:
112
+ if not all_res:
113
+ all_res = res
114
+ else:
115
+ all_res = [item for item in res if item in all_res]
116
+ items = remove_duplicates(all_res) if dedupe else all_res
105
117
  data['results'][output_name] = items
106
118
 
107
119
  # Save data
secator/rich.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import operator
2
+ from pathlib import Path
2
3
 
3
4
  import yaml
4
5
  from rich.console import Console
@@ -37,14 +38,18 @@ def status_to_color(value):
37
38
 
38
39
 
39
40
  FORMATTERS = {
40
- 'confidence': criticity_to_color,
41
+ 'confidence': lambda x: f'[dim]{x.upper()}[/]',
41
42
  'severity': criticity_to_color,
42
43
  'cvss_score': lambda score: '' if score == -1 else f'[bold cyan]{score}[/]',
43
44
  'port': lambda port: f'[bold cyan]{port}[/]',
44
- 'url': lambda host: f'[bold underline blue]{host}[/]',
45
+ 'url': lambda host: f'[bold underline blue link={host}]{host}[/]',
46
+ 'stored_response_path': lambda path: f'[link=file://{path}]:pencil:[/]' if path and Path(path).exists() else '',
47
+ 'screenshot_path': lambda path: f'[link=file://{path}]:camera:[/]' if path and Path(path).exists() else '',
45
48
  'ip': lambda ip: f'[bold yellow]{ip}[/]',
46
49
  'status_code': status_to_color,
47
- 'reference': lambda reference: f'[link={reference}]🡕[/]',
50
+ 'reference': lambda reference: f'[link={reference}]{reference}[/]' if reference else '',
51
+ 'matched_at': lambda matched_at: f'[link={matched_at}]{matched_at}[/]' if matched_at and matched_at.startswith('http') else '', # noqa: E501
52
+ 'match': lambda match: f'[link={match}]{match}[/]' if match else '',
48
53
  '_source': lambda source: f'[bold gold3]{source}[/]'
49
54
  }
50
55
 
@@ -89,13 +94,16 @@ def build_table(items, output_fields=[], exclude_fields=[], sort_by=None):
89
94
  key_str = key
90
95
  if not key.startswith('_'):
91
96
  key_str = ' '.join(key.split('_')).title()
92
- no_wrap = key in ['url', 'reference', 'references', 'matched_at']
93
- overflow = None if no_wrap else 'fold'
94
- table.add_column(
95
- key_str,
96
- overflow=overflow,
97
- min_width=10,
98
- no_wrap=no_wrap)
97
+ # TODO: remove this as it's not needed anymore
98
+ # no_wrap = key in ['url', 'reference', 'references', 'matched_at']
99
+ # overflow = None if no_wrap else 'fold'
100
+ # print('key: ', key_str, 'overflow: ', overflow, 'no_wrap: ', no_wrap)
101
+ # table.add_column(
102
+ # key_str,
103
+ # overflow=overflow,
104
+ # min_width=10,
105
+ # no_wrap=no_wrap)
106
+ table.add_column(key_str)
99
107
 
100
108
  if not keys:
101
109
  table.add_column(