secator 0.6.0__py3-none-any.whl → 0.8.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 (90) hide show
  1. secator/celery.py +160 -185
  2. secator/celery_utils.py +268 -0
  3. secator/cli.py +427 -176
  4. secator/config.py +114 -68
  5. secator/configs/workflows/host_recon.yaml +5 -3
  6. secator/configs/workflows/port_scan.yaml +7 -3
  7. secator/configs/workflows/subdomain_recon.yaml +2 -2
  8. secator/configs/workflows/url_bypass.yaml +10 -0
  9. secator/configs/workflows/url_dirsearch.yaml +1 -1
  10. secator/configs/workflows/url_vuln.yaml +1 -1
  11. secator/decorators.py +170 -92
  12. secator/definitions.py +11 -4
  13. secator/exporters/__init__.py +7 -5
  14. secator/exporters/console.py +10 -0
  15. secator/exporters/csv.py +27 -19
  16. secator/exporters/gdrive.py +16 -11
  17. secator/exporters/json.py +3 -1
  18. secator/exporters/table.py +30 -2
  19. secator/exporters/txt.py +20 -16
  20. secator/hooks/gcs.py +53 -0
  21. secator/hooks/mongodb.py +53 -27
  22. secator/installer.py +277 -60
  23. secator/output_types/__init__.py +29 -11
  24. secator/output_types/_base.py +11 -1
  25. secator/output_types/error.py +36 -0
  26. secator/output_types/exploit.py +12 -8
  27. secator/output_types/info.py +24 -0
  28. secator/output_types/ip.py +8 -1
  29. secator/output_types/port.py +9 -2
  30. secator/output_types/progress.py +5 -0
  31. secator/output_types/record.py +5 -3
  32. secator/output_types/stat.py +33 -0
  33. secator/output_types/subdomain.py +1 -1
  34. secator/output_types/tag.py +8 -6
  35. secator/output_types/target.py +2 -2
  36. secator/output_types/url.py +14 -11
  37. secator/output_types/user_account.py +6 -6
  38. secator/output_types/vulnerability.py +8 -6
  39. secator/output_types/warning.py +24 -0
  40. secator/report.py +56 -23
  41. secator/rich.py +44 -39
  42. secator/runners/_base.py +629 -638
  43. secator/runners/_helpers.py +5 -91
  44. secator/runners/celery.py +18 -0
  45. secator/runners/command.py +404 -214
  46. secator/runners/scan.py +8 -24
  47. secator/runners/task.py +21 -55
  48. secator/runners/workflow.py +41 -40
  49. secator/scans/__init__.py +28 -0
  50. secator/serializers/dataclass.py +6 -0
  51. secator/serializers/json.py +10 -5
  52. secator/serializers/regex.py +12 -4
  53. secator/tasks/_categories.py +147 -42
  54. secator/tasks/bbot.py +295 -0
  55. secator/tasks/bup.py +99 -0
  56. secator/tasks/cariddi.py +38 -49
  57. secator/tasks/dalfox.py +3 -0
  58. secator/tasks/dirsearch.py +14 -25
  59. secator/tasks/dnsx.py +49 -30
  60. secator/tasks/dnsxbrute.py +4 -1
  61. secator/tasks/feroxbuster.py +10 -20
  62. secator/tasks/ffuf.py +3 -2
  63. secator/tasks/fping.py +4 -4
  64. secator/tasks/gau.py +5 -0
  65. secator/tasks/gf.py +2 -2
  66. secator/tasks/gospider.py +4 -0
  67. secator/tasks/grype.py +11 -13
  68. secator/tasks/h8mail.py +32 -42
  69. secator/tasks/httpx.py +58 -21
  70. secator/tasks/katana.py +19 -23
  71. secator/tasks/maigret.py +27 -25
  72. secator/tasks/mapcidr.py +2 -3
  73. secator/tasks/msfconsole.py +22 -19
  74. secator/tasks/naabu.py +18 -2
  75. secator/tasks/nmap.py +82 -55
  76. secator/tasks/nuclei.py +13 -3
  77. secator/tasks/searchsploit.py +26 -11
  78. secator/tasks/subfinder.py +5 -1
  79. secator/tasks/wpscan.py +91 -94
  80. secator/template.py +61 -45
  81. secator/thread.py +24 -0
  82. secator/utils.py +417 -78
  83. secator/utils_test.py +48 -23
  84. secator/workflows/__init__.py +28 -0
  85. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/METADATA +59 -48
  86. secator-0.8.0.dist-info/RECORD +115 -0
  87. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
  88. secator-0.6.0.dist-info/RECORD +0 -101
  89. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
  90. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,23 @@
1
1
  import time
2
2
  from dataclasses import dataclass, field
3
+ from enum import Enum
3
4
 
4
5
  from secator.definitions import ALIVE, IP
5
6
  from secator.output_types import OutputType
6
7
  from secator.utils import rich_to_ansi
7
8
 
8
9
 
10
+ class IpProtocol(str, Enum):
11
+ IPv6 = 'IPv6'
12
+ IPv4 = 'IPv4'
13
+
14
+
9
15
  @dataclass
10
16
  class Ip(OutputType):
11
17
  ip: str
12
18
  host: str = ''
13
19
  alive: bool = False
20
+ protocol: str = field(default=IpProtocol.IPv4)
14
21
  _source: str = field(default='', repr=True)
15
22
  _type: str = field(default='ip', repr=True)
16
23
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
@@ -29,5 +36,5 @@ class Ip(OutputType):
29
36
  def __repr__(self) -> str:
30
37
  s = f'💻 [bold white]{self.ip}[/]'
31
38
  if self.host:
32
- s += f' \[[bold magenta]{self.host}[/]]'
39
+ s += rf' \[[bold magenta]{self.host}[/]]'
33
40
  return rich_to_ansi(s)
@@ -14,7 +14,9 @@ class Port(OutputType):
14
14
  service_name: str = field(default='', compare=False)
15
15
  cpes: list = field(default_factory=list, compare=False)
16
16
  host: str = field(default='', repr=True, compare=False)
17
+ protocol: str = field(default='tcp', repr=True, compare=False)
17
18
  extra_data: dict = field(default_factory=dict, compare=False)
19
+ confidence: str = field(default='low', repr=False, compare=False)
18
20
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
19
21
  _source: str = field(default='', repr=True, compare=False)
20
22
  _type: str = field(default='port', repr=True)
@@ -38,8 +40,13 @@ class Port(OutputType):
38
40
 
39
41
  def __repr__(self) -> str:
40
42
  s = f'🔓 {self.ip}:[bold red]{self.port:<4}[/] [bold yellow]{self.state.upper()}[/]'
43
+ if self.protocol != 'TCP':
44
+ s += rf' \[[yellow3]{self.protocol}[/]]'
41
45
  if self.service_name:
42
- s += f' \[[bold purple]{self.service_name}[/]]'
46
+ conf = ''
47
+ if self.confidence == 'low':
48
+ conf = '?'
49
+ s += rf' \[[bold purple]{self.service_name}{conf}[/]]'
43
50
  if self.host:
44
- s += f' \[[cyan]{self.host}[/]]'
51
+ s += rf' \[[cyan]{self.host}[/]]'
45
52
  return rich_to_ansi(s)
@@ -23,6 +23,11 @@ class Progress(OutputType):
23
23
  _table_fields = ['percent', 'duration']
24
24
  _sort_by = ('percent',)
25
25
 
26
+ def __post_init__(self):
27
+ super().__post_init__()
28
+ if not 0 <= self.percent <= 100:
29
+ self.percent = 0
30
+
26
31
  def __str__(self) -> str:
27
32
  return f'{self.percent}%'
28
33
 
@@ -3,7 +3,7 @@ from dataclasses import dataclass, field
3
3
 
4
4
  from secator.definitions import HOST, NAME, TYPE
5
5
  from secator.output_types import OutputType
6
- from secator.utils import rich_to_ansi
6
+ from secator.utils import rich_to_ansi, rich_escape as _s
7
7
 
8
8
 
9
9
  @dataclass
@@ -28,7 +28,9 @@ class Record(OutputType):
28
28
  return self.name
29
29
 
30
30
  def __repr__(self) -> str:
31
- s = f'🎤 [bold white]{self.name}[/] \[[green]{self.type}[/]] \[[magenta]{self.host}[/]]'
31
+ s = rf'🎤 [bold white]{self.name}[/] \[[green]{self.type}[/]]'
32
+ if self.host:
33
+ s += rf' \[[magenta]{self.host}[/]]'
32
34
  if self.extra_data:
33
- s += ' \[[bold yellow]' + ','.join(f'{k}={v}' for k, v in self.extra_data.items()) + '[/]]'
35
+ s += r' \[[bold yellow]' + ','.join(f'{_s(k)}={_s(v)}' for k, v in self.extra_data.items()) + '[/]]'
34
36
  return rich_to_ansi(s)
@@ -0,0 +1,33 @@
1
+ import time
2
+ from dataclasses import dataclass, field
3
+
4
+ from secator.output_types import OutputType
5
+ from secator.utils import rich_to_ansi
6
+
7
+
8
+ @dataclass
9
+ class Stat(OutputType):
10
+ name: str
11
+ pid: int
12
+ cpu: int
13
+ memory: int
14
+ net_conns: int = field(default=None, repr=True)
15
+ extra_data: dict = field(default_factory=dict)
16
+ _source: str = field(default='', repr=True)
17
+ _type: str = field(default='stat', repr=True)
18
+ _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
19
+ _uuid: str = field(default='', repr=True, compare=False)
20
+ _context: dict = field(default_factory=dict, repr=True, compare=False)
21
+ _tagged: bool = field(default=False, repr=True, compare=False)
22
+ _duplicate: bool = field(default=False, repr=True, compare=False)
23
+ _related: list = field(default_factory=list, compare=False)
24
+
25
+ _table_fields = ['name', 'pid', 'cpu', 'memory']
26
+ _sort_by = ('name', 'pid')
27
+
28
+ def __repr__(self) -> str:
29
+ s = rf'[dim yellow3]📊 {self.name} \[pid={self.pid}] \[cpu={self.cpu:.2f}%] \[memory={self.memory:.2f}%]'
30
+ if self.net_conns:
31
+ s += rf' \[connections={self.net_conns}]'
32
+ s += ' [/]'
33
+ return rich_to_ansi(s)
@@ -38,5 +38,5 @@ class Subdomain(OutputType):
38
38
  if sources_str:
39
39
  s += f' [{sources_str}]'
40
40
  if self.extra_data:
41
- s += ' \[[bold yellow]' + ', '.join(f'{k}:{v}' for k, v in self.extra_data.items()) + '[/]]'
41
+ s += r' \[[bold yellow]' + ', '.join(f'{k}:{v}' for k, v in self.extra_data.items()) + '[/]]'
42
42
  return rich_to_ansi(s)
@@ -2,7 +2,7 @@ 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
5
+ from secator.utils import rich_to_ansi, trim_string, rich_escape as _s
6
6
 
7
7
 
8
8
  @dataclass
@@ -30,17 +30,19 @@ class Tag(OutputType):
30
30
 
31
31
  def __repr__(self) -> str:
32
32
  s = f'🏷️ [bold magenta]{self.name}[/]'
33
- s += f' found @ [bold]{self.match}[/]'
33
+ s += f' found @ [bold]{_s(self.match)}[/]'
34
34
  ed = ''
35
35
  if self.extra_data:
36
36
  for k, v in self.extra_data.items():
37
37
  sep = ' '
38
38
  if not v:
39
39
  continue
40
- if len(v) >= 80:
41
- v = v.replace('\n', '\n' + ' ').replace('...TRUNCATED', '\n[italic bold red]...truncated to 1000 chars[/]')
42
- sep = '\n '
43
- ed += f'\n [dim red]{k}[/]:{sep}[dim yellow]{v}[/]'
40
+ if isinstance(v, str):
41
+ v = trim_string(v, max_length=1000)
42
+ if len(v) > 1000:
43
+ v = v.replace('\n', '\n' + sep)
44
+ sep = '\n '
45
+ ed += f'\n [dim red]{_s(k)}[/]:{sep}[dim yellow]{_s(v)}[/]'
44
46
  if ed:
45
47
  s += ed
46
48
  return rich_to_ansi(s)
@@ -2,7 +2,7 @@ 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
5
+ from secator.utils import rich_to_ansi, rich_escape as _s
6
6
 
7
7
 
8
8
  @dataclass
@@ -26,5 +26,5 @@ class Target(OutputType):
26
26
  return self.name
27
27
 
28
28
  def __repr__(self):
29
- s = f'🎯 {self.name}'
29
+ s = f'🎯 {_s(self.name)}'
30
30
  return rich_to_ansi(s)
@@ -4,7 +4,8 @@ from dataclasses import dataclass, field
4
4
  from secator.definitions import (CONTENT_LENGTH, CONTENT_TYPE, STATUS_CODE,
5
5
  TECH, TIME, TITLE, URL, WEBSERVER)
6
6
  from secator.output_types import OutputType
7
- from secator.utils import rich_to_ansi
7
+ from secator.utils import rich_to_ansi, trim_string, rich_escape as _s
8
+ from secator.config import CONFIG
8
9
 
9
10
 
10
11
  @dataclass
@@ -55,25 +56,27 @@ class Url(OutputType):
55
56
  return self.url
56
57
 
57
58
  def __repr__(self):
58
- s = f'🔗 [white]{self.url}'
59
+ s = f'🔗 [white]{_s(self.url)}'
59
60
  if self.method and self.method != 'GET':
60
- s += f' \[[turquoise4]{self.method}[/]]'
61
+ s += rf' \[[turquoise4]{self.method}[/]]'
61
62
  if self.status_code and self.status_code != 0:
62
63
  if self.status_code < 400:
63
- s += f' \[[green]{self.status_code}[/]]'
64
+ s += rf' \[[green]{self.status_code}[/]]'
64
65
  else:
65
- s += f' \[[red]{self.status_code}[/]]'
66
+ s += rf' \[[red]{self.status_code}[/]]'
66
67
  if self.title:
67
- s += f' \[[green]{self.title}[/]]'
68
+ s += rf' \[[green]{trim_string(self.title)}[/]]'
68
69
  if self.webserver:
69
- s += f' \[[magenta]{self.webserver}[/]]'
70
+ s += rf' \[[magenta]{_s(self.webserver)}[/]]'
70
71
  if self.tech:
71
- techs_str = ', '.join([f'[magenta]{tech}[/]' for tech in self.tech])
72
+ techs_str = ', '.join([f'[magenta]{_s(tech)}[/]' for tech in self.tech])
72
73
  s += f' [{techs_str}]'
73
74
  if self.content_type:
74
- s += f' \[[magenta]{self.content_type}[/]]'
75
+ s += rf' \[[magenta]{_s(self.content_type)}[/]]'
75
76
  if self.content_length:
76
- s += f' \[[magenta]{self.content_length}[/]]'
77
+ cl = str(self.content_length)
78
+ cl += '[bold red]+[/]' if self.content_length == CONFIG.http.response_max_size_bytes else ''
79
+ s += rf' \[[magenta]{cl}[/]]'
77
80
  if self.screenshot_path:
78
- s += f' \[[magenta]{self.screenshot_path}[/]]'
81
+ s += rf' \[[magenta]{_s(self.screenshot_path)}[/]]'
79
82
  return rich_to_ansi(s)
@@ -3,7 +3,7 @@ from dataclasses import dataclass, field
3
3
 
4
4
  from secator.definitions import SITE_NAME, URL, USERNAME
5
5
  from secator.output_types import OutputType
6
- from secator.utils import rich_to_ansi
6
+ from secator.utils import rich_to_ansi, rich_escape as _s
7
7
 
8
8
 
9
9
  @dataclass
@@ -29,13 +29,13 @@ class UserAccount(OutputType):
29
29
  return self.url
30
30
 
31
31
  def __repr__(self) -> str:
32
- s = f'👤 [green]{self.username}[/]'
32
+ s = f'👤 [green]{_s(self.username)}[/]'
33
33
  if self.email:
34
- s += f' \[[bold yellow]{self.email}[/]]'
34
+ s += rf' \[[bold yellow]{_s(self.email)}[/]]'
35
35
  if self.site_name:
36
- s += f' \[[bold blue]{self.site_name}[/]]'
36
+ s += rf' \[[bold blue]{self.site_name}[/]]'
37
37
  if self.url:
38
- s += f' \[[white]{self.url}[/]]'
38
+ s += rf' \[[white]{_s(self.url)}[/]]'
39
39
  if self.extra_data:
40
- s += ' \[[bold yellow]' + ', '.join(f'{k}:{v}' for k, v in self.extra_data.items()) + '[/]]'
40
+ s += r' \[[bold yellow]' + _s(', '.join(f'{k}:{v}' for k, v in self.extra_data.items()) + '[/]]')
41
41
  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
8
+ from secator.utils import rich_to_ansi, rich_escape as _s
9
9
 
10
10
 
11
11
  @dataclass
@@ -58,7 +58,8 @@ class Vulnerability(OutputType):
58
58
  'unknown': 5,
59
59
  None: 6
60
60
  }
61
- self.severity_nb = severity_map[self.severity]
61
+ self.severity = self.severity.lower() # normalize severity
62
+ self.severity_nb = severity_map.get(self.severity, 6)
62
63
  self.confidence_nb = severity_map[self.confidence]
63
64
  if len(self.references) > 0:
64
65
  self.reference = self.references[0]
@@ -69,6 +70,7 @@ class Vulnerability(OutputType):
69
70
  data = ','.join(data['data'])
70
71
  elif isinstance(data, dict):
71
72
  data = ', '.join([f'{k}:{v}' for k, v in data.items()])
73
+ data = _s(data)
72
74
  tags = self.tags
73
75
  colors = {
74
76
  'critical': 'bold red',
@@ -78,13 +80,13 @@ class Vulnerability(OutputType):
78
80
  'info': 'magenta',
79
81
  'unknown': 'dim magenta'
80
82
  }
81
- c = colors[self.severity]
82
- s = f'🚨 \[[green]{self.name} [link={self.reference}]🡕[/link][/]] \[[{c}]{self.severity}[/]] {self.matched_at}'
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
83
85
  if tags:
84
86
  tags_str = ','.join(tags)
85
- s += f' \[[cyan]{tags_str}[/]]'
87
+ s += rf' \[[cyan]{_s(tags_str)}[/]]'
86
88
  if data:
87
- s += f' \[[yellow]{str(data)}[/]]'
89
+ s += rf' \[[yellow]{str(data)}[/]]'
88
90
  if self.confidence == 'low':
89
91
  s = f'[dim]{s}[/]'
90
92
  return rich_to_ansi(s)
@@ -0,0 +1,24 @@
1
+ from dataclasses import dataclass, field
2
+ import time
3
+ from secator.output_types import OutputType
4
+ from secator.utils import rich_to_ansi, rich_escape as _s
5
+
6
+
7
+ @dataclass
8
+ class Warning(OutputType):
9
+ message: str
10
+ task_id: str = field(default='', compare=False)
11
+ _source: str = field(default='', repr=True)
12
+ _type: str = field(default='warning', repr=True)
13
+ _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
14
+ _uuid: str = field(default='', repr=True, compare=False)
15
+ _context: dict = field(default_factory=dict, repr=True, compare=False)
16
+ _duplicate: bool = field(default=False, repr=True, compare=False)
17
+ _related: list = field(default_factory=list, compare=False)
18
+
19
+ _table_fields = ['task_name', 'message']
20
+ _sort_by = ('_timestamp',)
21
+
22
+ def __repr__(self):
23
+ s = rf"\[[yellow]WRN[/]] {_s(self.message)}"
24
+ return rich_to_ansi(s)
secator/report.py CHANGED
@@ -1,9 +1,31 @@
1
1
  import operator
2
2
 
3
3
  from secator.config import CONFIG
4
- from secator.output_types import OUTPUT_TYPES, OutputType
5
- from secator.utils import merge_opts, get_file_timestamp
4
+ from secator.output_types import FINDING_TYPES, OutputType
5
+ from secator.utils import merge_opts, get_file_timestamp, traceback_as_string
6
6
  from secator.rich import console
7
+ from secator.runners._helpers import extract_from_results
8
+
9
+ import concurrent.futures
10
+ from threading import Lock
11
+
12
+
13
+ def remove_duplicates(objects):
14
+ unique_objects = []
15
+ lock = Lock()
16
+
17
+ def add_if_unique(obj):
18
+ nonlocal unique_objects
19
+ with lock:
20
+ # Perform linear search to check for duplicates
21
+ if all(obj != existing_obj for existing_obj in unique_objects):
22
+ unique_objects.append(obj)
23
+
24
+ with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
25
+ # Execute the function concurrently for each object
26
+ executor.map(add_if_unique, objects)
27
+
28
+ return unique_objects
7
29
 
8
30
 
9
31
  # TODO: initialize from data, not from runner
@@ -29,54 +51,65 @@ class Report:
29
51
  report_cls(self).send()
30
52
  except Exception as e:
31
53
  console.print(
32
- f'Could not create exporter {report_cls.__name__} for {self.__class__.__name__}: {str(e)}',
33
- style='bold red')
54
+ f'[bold red]Could not create exporter {report_cls.__name__} for {self.__class__.__name__}: '
55
+ f'{str(e)}[/]\n[dim]{traceback_as_string(e)}[/]',
56
+ )
34
57
 
35
- def build(self):
58
+ def build(self, extractors=[], dedupe=False):
36
59
  # Trim options
37
60
  from secator.decorators import DEFAULT_CLI_OPTIONS
38
61
  opts = merge_opts(self.runner.config.options, self.runner.run_opts)
39
62
  opts = {
40
63
  k: v for k, v in opts.items()
41
- if k not in DEFAULT_CLI_OPTIONS
42
- and not k.startswith('print_')
64
+ if k not in DEFAULT_CLI_OPTIONS and k not in self.runner.print_opts
43
65
  and v is not None
44
66
  }
67
+ runner_fields = {
68
+ 'name',
69
+ 'status',
70
+ 'targets',
71
+ 'start_time',
72
+ 'end_time',
73
+ 'elapsed',
74
+ 'elapsed_human',
75
+ 'run_opts',
76
+ 'results_count'
77
+ }
45
78
 
46
79
  # Prepare report structure
47
80
  data = {
48
- 'info': {
49
- 'title': self.title,
50
- 'runner': self.runner.__class__.__name__,
51
- 'name': self.runner.config.name,
52
- 'targets': self.runner.targets,
53
- 'total_time': str(self.runner.elapsed),
54
- 'total_human': self.runner.elapsed_human,
55
- 'opts': opts,
56
- },
57
- 'results': {},
81
+ 'info': {k: v for k, v in self.runner.toDict().items() if k in runner_fields},
82
+ 'results': {}
58
83
  }
84
+ if 'results' in data['info']:
85
+ del data['info']['results']
86
+ data['info']['title'] = self.title
87
+ data['info']['errors'] = self.runner.errors
59
88
 
60
89
  # Fill report
61
- for output_type in OUTPUT_TYPES:
62
- if output_type.__name__ == 'Progress':
63
- continue
90
+ for output_type in FINDING_TYPES:
64
91
  output_name = output_type.get_name()
65
92
  sort_by, _ = get_table_fields(output_type)
66
93
  items = [
67
94
  item for item in self.runner.results
68
95
  if isinstance(item, OutputType) and item._type == output_name
69
96
  ]
70
- if CONFIG.runners.remove_duplicates:
71
- items = [item for item in items if not item._duplicate]
72
97
  if items:
73
98
  if sort_by and all(sort_by):
74
99
  items = sorted(items, key=operator.attrgetter(*sort_by))
100
+ if dedupe and CONFIG.runners.remove_duplicates:
101
+ 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])
75
105
  data['results'][output_name] = items
76
106
 
77
107
  # Save data
78
108
  self.data = data
79
109
 
110
+ def is_empty(self):
111
+ return all(not items for items in self.data['results'].values())
112
+
80
113
 
81
114
  def get_table_fields(output_type):
82
115
  """Get output fields and sort fields based on output type.
@@ -89,7 +122,7 @@ def get_table_fields(output_type):
89
122
  """
90
123
  sort_by = ()
91
124
  output_fields = []
92
- if output_type in OUTPUT_TYPES:
125
+ if output_type in FINDING_TYPES:
93
126
  sort_by = output_type._sort_by
94
127
  output_fields = output_type._table_fields
95
128
  return sort_by, output_fields
secator/rich.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import operator
2
2
 
3
3
  import yaml
4
- from rich import box
5
4
  from rich.console import Console
6
5
  from rich.table import Table
7
6
 
@@ -67,51 +66,57 @@ def build_table(items, output_fields=[], exclude_fields=[], sort_by=None):
67
66
  items = sorted(items, key=operator.attrgetter(*sort_by))
68
67
 
69
68
  # Create rich table
70
- box_style = box.ROUNDED
71
- table = Table(show_lines=True, box=box_style)
69
+ table = Table(show_lines=True)
72
70
 
73
71
  # Get table schema if any, default to first item keys
74
- keys = output_fields
75
-
76
- # List of fields to exclude
77
- keys = [k for k in keys if k not in exclude_fields]
78
-
79
- # Remove meta fields not needed in output
80
- if '_cls' in keys:
81
- keys.remove('_cls')
82
- if '_type' in keys:
83
- keys.remove('_type')
84
- if '_uuid' in keys:
85
- keys.remove('_uuid')
86
-
87
- # Add _source field
88
- if '_source' not in keys:
89
- keys.append('_source')
90
-
91
- # Create table columns
92
- for key in keys:
93
- key_str = key
94
- if not key.startswith('_'):
95
- key_str = ' '.join(key.split('_')).upper()
96
- no_wrap = key in ['url', 'reference', 'references', 'matched_at']
97
- overflow = None if no_wrap else 'fold'
72
+ keys = []
73
+ if output_fields:
74
+ keys = [k for k in output_fields if k not in exclude_fields]
75
+ # Remove meta fields not needed in output
76
+ if '_cls' in keys:
77
+ keys.remove('_cls')
78
+ if '_type' in keys:
79
+ keys.remove('_type')
80
+ if '_uuid' in keys:
81
+ keys.remove('_uuid')
82
+
83
+ # Add _source field
84
+ if '_source' not in keys:
85
+ keys.append('_source')
86
+
87
+ # Create table columns
88
+ for key in keys:
89
+ key_str = key
90
+ if not key.startswith('_'):
91
+ 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)
99
+
100
+ if not keys:
98
101
  table.add_column(
99
- key_str,
100
- overflow=overflow,
102
+ 'Extracted values',
103
+ overflow=False,
101
104
  min_width=10,
102
- no_wrap=no_wrap,
103
- header_style='bold blue')
105
+ no_wrap=False)
104
106
 
105
107
  # Create table rows
106
108
  for item in items:
107
109
  values = []
108
- for key in keys:
109
- value = getattr(item, key)
110
- value = FORMATTERS.get(key, lambda x: x)(value)
111
- if isinstance(value, dict) or isinstance(value, list):
112
- value = yaml.dump(value)
113
- elif isinstance(value, int) or isinstance(value, float):
114
- value = str(value)
115
- values.append(value)
110
+ if keys:
111
+ for key in keys:
112
+ value = getattr(item, key) if keys else item
113
+ value = FORMATTERS.get(key, lambda x: x)(value) if keys else item
114
+ if isinstance(value, dict) or isinstance(value, list):
115
+ value = yaml.dump(value)
116
+ elif isinstance(value, int) or isinstance(value, float):
117
+ value = str(value)
118
+ values.append(value)
119
+ else:
120
+ values = [item]
116
121
  table.add_row(*values)
117
122
  return table