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
secator/config.py CHANGED
@@ -20,6 +20,7 @@ StrExpandHome = Annotated[str, AfterValidator(lambda v: v.replace('~', str(Path.
20
20
  ROOT_FOLDER = Path(__file__).parent.parent
21
21
  LIB_FOLDER = ROOT_FOLDER / 'secator'
22
22
  CONFIGS_FOLDER = LIB_FOLDER / 'configs'
23
+ DATA_FOLDER = os.environ.get('SECATOR_DIRS_DATA') or str(Path.home() / '.secator')
23
24
 
24
25
 
25
26
  class StrictModel(BaseModel, extra='forbid'):
@@ -28,12 +29,14 @@ class StrictModel(BaseModel, extra='forbid'):
28
29
 
29
30
  class Directories(StrictModel):
30
31
  bin: Directory = Path.home() / '.local' / 'bin'
31
- data: Directory = Path.home() / '.secator'
32
+ share: Directory = Path.home() / '.local' / 'share'
33
+ data: Directory = Path(DATA_FOLDER)
32
34
  templates: Directory = ''
33
35
  reports: Directory = ''
34
36
  wordlists: Directory = ''
35
37
  cves: Directory = ''
36
38
  payloads: Directory = ''
39
+ performance: Directory = ''
37
40
  revshells: Directory = ''
38
41
  celery: Directory = ''
39
42
  celery_data: Directory = ''
@@ -42,7 +45,7 @@ class Directories(StrictModel):
42
45
  @model_validator(mode='after')
43
46
  def set_default_folders(self) -> Self:
44
47
  """Set folders to be relative to the data folders if they are unspecified in config."""
45
- for folder in ['templates', 'reports', 'wordlists', 'cves', 'payloads', 'revshells', 'celery', 'celery_data', 'celery_results']: # noqa: E501
48
+ for folder in ['templates', 'reports', 'wordlists', 'cves', 'payloads', 'performance', 'revshells', 'celery', 'celery_data', 'celery_results']: # noqa: E501
46
49
  rel_target = '/'.join(folder.split('_'))
47
50
  val = getattr(self, folder) or self.data / rel_target
48
51
  setattr(self, folder, val)
@@ -61,40 +64,52 @@ class Celery(StrictModel):
61
64
  broker_visibility_timeout: int = 3600
62
65
  override_default_logging: bool = True
63
66
  result_backend: StrExpandHome = ''
67
+ result_expires: int = 86400 # 1 day
64
68
 
65
69
 
66
70
  class Cli(StrictModel):
67
- github_token: str = ''
71
+ github_token: str = os.environ.get('GITHUB_TOKEN', '')
68
72
  record: bool = False
69
73
  stdin_timeout: int = 1000
70
74
 
71
75
 
72
76
  class Runners(StrictModel):
73
- input_chunk_size: int = 1000
74
- progress_update_frequency: int = 60
77
+ input_chunk_size: int = 100
78
+ progress_update_frequency: int = 20
79
+ stat_update_frequency: int = 20
80
+ backend_update_frequency: int = 5
81
+ poll_frequency: int = 5
75
82
  skip_cve_search: bool = False
76
- skip_cve_low_confidence: bool = True
83
+ skip_exploit_search: bool = False
84
+ skip_cve_low_confidence: bool = False
77
85
  remove_duplicates: bool = False
78
86
 
79
87
 
88
+ class Security(StrictModel):
89
+ allow_local_file_access: bool = True
90
+ auto_install_commands: bool = True
91
+ force_source_install: bool = False
92
+
93
+
80
94
  class HTTP(StrictModel):
81
95
  socks5_proxy: str = 'socks5://127.0.0.1:9050'
82
96
  http_proxy: str = 'https://127.0.0.1:9080'
83
97
  store_responses: bool = False
98
+ response_max_size_bytes: int = 100000 # 100MB
84
99
  proxychains_command: str = 'proxychains'
85
100
  freeproxy_timeout: int = 1
86
101
 
87
102
 
88
103
  class Tasks(StrictModel):
89
- exporters: List[str] = ['json', 'csv']
104
+ exporters: List[str] = ['json', 'csv', 'txt']
90
105
 
91
106
 
92
107
  class Workflows(StrictModel):
93
- exporters: List[str] = ['json', 'csv']
108
+ exporters: List[str] = ['json', 'csv', 'txt']
94
109
 
95
110
 
96
111
  class Scans(StrictModel):
97
- exporters: List[str] = ['json', 'csv']
112
+ exporters: List[str] = ['json', 'csv', 'txt']
98
113
 
99
114
 
100
115
  class Payloads(StrictModel):
@@ -109,17 +124,24 @@ class Wordlists(StrictModel):
109
124
  defaults: Dict[str, str] = {'http': 'bo0m_fuzz', 'dns': 'combined_subdomains'}
110
125
  templates: Dict[str, str] = {
111
126
  'bo0m_fuzz': 'https://raw.githubusercontent.com/Bo0oM/fuzz.txt/master/fuzz.txt',
112
- 'combined_subdomains': 'https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/combined_subdomains.txt' # noqa: E501
127
+ 'combined_subdomains': 'https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/DNS/combined_subdomains.txt', # noqa: E501
128
+ 'directory_list_small': 'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Discovery/Web-Content/directory-list-2.3-small.txt', # noqa: E501
113
129
  }
114
130
  lists: Dict[str, List[str]] = {}
115
131
 
116
132
 
117
- class GoogleAddon(StrictModel):
133
+ class GoogleDriveAddon(StrictModel):
118
134
  enabled: bool = False
119
135
  drive_parent_folder_id: str = ''
120
136
  credentials_path: str = ''
121
137
 
122
138
 
139
+ class GoogleCloudStorageAddon(StrictModel):
140
+ enabled: bool = False
141
+ bucket_name: str = ''
142
+ credentials_path: str = ''
143
+
144
+
123
145
  class WorkerAddon(StrictModel):
124
146
  enabled: bool = False
125
147
 
@@ -128,10 +150,13 @@ class MongodbAddon(StrictModel):
128
150
  enabled: bool = False
129
151
  url: str = 'mongodb://localhost'
130
152
  update_frequency: int = 60
153
+ max_pool_size: int = 10
154
+ server_selection_timeout_ms: int = 5000
131
155
 
132
156
 
133
157
  class Addons(StrictModel):
134
- google: GoogleAddon = GoogleAddon()
158
+ gdrive: GoogleDriveAddon = GoogleDriveAddon()
159
+ gcs: GoogleCloudStorageAddon = GoogleCloudStorageAddon()
135
160
  worker: WorkerAddon = WorkerAddon()
136
161
  mongodb: MongodbAddon = MongodbAddon()
137
162
 
@@ -149,6 +174,7 @@ class SecatorConfig(StrictModel):
149
174
  payloads: Payloads = Payloads()
150
175
  wordlists: Wordlists = Wordlists()
151
176
  addons: Addons = Addons()
177
+ security: Security = Security()
152
178
  offline_mode: bool = False
153
179
 
154
180
 
@@ -161,7 +187,7 @@ class Config(DotMap):
161
187
  >>> config = Config.parse(path='/path/to/config.yml') # get custom config (from YAML file).
162
188
  >>> config.print() # print config without defaults.
163
189
  >>> config.print(partial=False) # print full config.
164
- >>> config.set('addons.google.enabled', False) # set value in config.
190
+ >>> config.set('addons.gdrive.enabled', False) # set value in config.
165
191
  >>> config.save() # save config back to disk.
166
192
  """
167
193
 
@@ -479,56 +505,81 @@ def download_files(data: dict, target_folder: Path, offline_mode: bool, type: st
479
505
  offline_mode (bool): Offline mode.
480
506
  """
481
507
  for name, url_or_path in data.items():
482
- if url_or_path.startswith('git+'):
483
- # Clone Git repository
484
- git_url = url_or_path[4:] # remove 'git+' prefix
485
- repo_name = git_url.split('/')[-1]
486
- if repo_name.endswith('.git'):
487
- repo_name = repo_name[:-4]
488
- target_path = target_folder / repo_name
489
- if not target_path.exists():
490
- console.print(f'[bold turquoise4]Cloning git {type} [bold magenta]{repo_name}[/] ...[/] ', end='')
508
+ target_path = download_file(url_or_path, target_folder, offline_mode, type, name=name)
509
+ if target_path:
510
+ data[name] = target_path
511
+
512
+
513
+ def download_file(url_or_path, target_folder: Path, offline_mode: bool, type: str, name: str = None):
514
+ """Download remote file to target folder, clone git repos, or symlink local files.
515
+
516
+ Args:
517
+ data (dict): Dict of name to url or local path prefixed with 'git+' for Git repos.
518
+ target_folder (Path): Target folder for storing files or repos.
519
+ offline_mode (bool): Offline mode.
520
+ type (str): Type of files to handle.
521
+ name (str, Optional): Name of object.
522
+
523
+ Returns:
524
+ path (Path): Path to downloaded file / folder.
525
+ """
526
+ if url_or_path.startswith('git+'):
527
+ # Clone Git repository
528
+ git_url = url_or_path[4:] # remove 'git+' prefix
529
+ repo_name = git_url.split('/')[-1]
530
+ if repo_name.endswith('.git'):
531
+ repo_name = repo_name[:-4]
532
+ target_path = target_folder / repo_name
533
+ if not target_path.exists():
534
+ console.print(f'[bold turquoise4]Cloning git {type} [bold magenta]{repo_name}[/] ...[/] ', end='')
535
+ if offline_mode:
536
+ console.print('[bold orange1]skipped [dim][offline[/].[/]')
537
+ return
538
+ try:
539
+ call(['git', 'clone', git_url, str(target_path)], stderr=DEVNULL, stdout=DEVNULL)
540
+ console.print('[bold green]ok.[/]')
541
+ except Exception as e:
542
+ console.print(f'[bold red]failed ({str(e)}).[/]')
543
+ return target_path.resolve()
544
+ elif Path(url_or_path).exists():
545
+ # Create a symbolic link for a local file
546
+ local_path = Path(url_or_path)
547
+ target_path = target_folder / local_path.name
548
+ if not name:
549
+ name = url_or_path.split('/')[-1]
550
+ if not CONFIG.security.allow_local_file_access:
551
+ console.print(f'[bold red]Cannot reference local file {url_or_path}(disabled for security reasons)[/]')
552
+ return
553
+ if not target_path.exists():
554
+ console.print(f'[bold turquoise4]Symlinking {type} [bold magenta]{name}[/] ...[/] ', end='')
555
+ try:
556
+ target_path.symlink_to(local_path)
557
+ console.print('[bold green]ok.[/]')
558
+ except Exception as e:
559
+ console.print(f'[bold red]failed ({str(e)}).[/]')
560
+ return target_path.resolve()
561
+ else:
562
+ # Download file from URL
563
+ ext = url_or_path.split('.')[-1]
564
+ if not name:
565
+ name = url_or_path.split('/')[-1]
566
+ filename = f'{name}.{ext}' if not name.endswith(ext) else name
567
+ target_path = target_folder / filename
568
+ if not target_path.exists():
569
+ try:
570
+ console.print(f'[bold turquoise4]Downloading {type} [bold magenta]{filename}[/] ...[/] ', end='')
491
571
  if offline_mode:
492
- console.print('[bold orange1]skipped [dim][offline[/].[/]')
493
- continue
494
- try:
495
- call(['git', 'clone', git_url, str(target_path)], stderr=DEVNULL, stdout=DEVNULL)
496
- console.print('[bold green]ok.[/]')
497
- except Exception as e:
498
- console.print(f'[bold red]failed ({str(e)}).[/]')
499
- data[name] = target_path.resolve()
500
- elif Path(url_or_path).exists():
501
- # Create a symbolic link for a local file
502
- local_path = Path(url_or_path)
503
- target_path = target_folder / local_path.name
504
- if not target_path.exists():
505
- console.print(f'[bold turquoise4]Symlinking {type} [bold magenta]{name}[/] ...[/] ', end='')
506
- try:
507
- target_path.symlink_to(local_path)
508
- console.print('[bold green]ok.[/]')
509
- except Exception as e:
510
- console.print(f'[bold red]failed ({str(e)}).[/]')
511
- data[name] = target_path.resolve()
512
- else:
513
- # Download file from URL
514
- ext = url_or_path.split('.')[-1]
515
- filename = f'{name}.{ext}'
516
- target_path = target_folder / filename
517
- if not target_path.exists():
518
- try:
519
- console.print(f'[bold turquoise4]Downloading {type} [bold magenta]{filename}[/] ...[/] ', end='')
520
- if offline_mode:
521
- console.print('[bold orange1]skipped [dim](offline)[/].[/]')
522
- continue
523
- resp = requests.get(url_or_path, timeout=3)
524
- resp.raise_for_status()
525
- with open(target_path, 'wb') as f:
526
- f.write(resp.content)
527
- console.print('[bold green]ok.[/]')
528
- except requests.RequestException as e:
529
- console.print(f'[bold red]failed ({str(e)}).[/]')
530
- continue
531
- data[name] = target_path.resolve()
572
+ console.print('[bold orange1]skipped [dim](offline)[/].[/]')
573
+ return
574
+ resp = requests.get(url_or_path, timeout=3)
575
+ resp.raise_for_status()
576
+ with open(target_path, 'wb') as f:
577
+ f.write(resp.content)
578
+ console.print('[bold green]ok.[/]')
579
+ except requests.RequestException as e:
580
+ console.print(f'[bold red]failed ({str(e)}).[/]')
581
+ return
582
+ return target_path.resolve()
532
583
 
533
584
 
534
585
  # Load default_config
@@ -560,13 +611,8 @@ for name, dir in CONFIG.dirs.items():
560
611
  dir.mkdir(parents=False)
561
612
  console.print('[bold green]ok.[/]')
562
613
 
563
- # Download wordlists and set defaults
614
+ # Download wordlists and payloads
564
615
  download_files(CONFIG.wordlists.templates, CONFIG.dirs.wordlists, CONFIG.offline_mode, 'wordlist')
565
- for category, name in CONFIG.wordlists.defaults.items():
566
- if name in CONFIG.wordlists.templates.keys():
567
- CONFIG.wordlists.defaults[category] = str(CONFIG.wordlists.templates[name])
568
-
569
- # Download payloads
570
616
  download_files(CONFIG.payloads.templates, CONFIG.dirs.payloads, CONFIG.offline_mode, 'payload')
571
617
 
572
618
  # Print config
@@ -11,6 +11,8 @@ tasks:
11
11
  description: Find open ports
12
12
  nmap:
13
13
  description: Search for vulnerabilities on open ports
14
+ skip_host_discovery: True
15
+ version_detection: True
14
16
  targets_: port.host
15
17
  ports_: port.port
16
18
  httpx:
@@ -18,7 +20,7 @@ tasks:
18
20
  targets_:
19
21
  - type: port
20
22
  field: '{host}:{port}'
21
- condition: item._source == 'nmap'
23
+ condition: item._source.startswith('nmap')
22
24
  _group:
23
25
  nuclei/network:
24
26
  description: Scan network and SSL vulnerabilities
@@ -32,10 +34,10 @@ tasks:
32
34
  condition: item.status_code != 0
33
35
  results:
34
36
  - type: port
35
- condition: item._source == 'nmap'
37
+ condition: item._source.startswith('nmap')
36
38
 
37
39
  - type: vulnerability
38
40
  # condition: item.confidence == 'high'
39
41
 
40
42
  - type: url
41
- condition: item.status_code != 0
43
+ condition: item.status_code != 0
@@ -5,11 +5,15 @@ description: Port scan
5
5
  tags: [recon, network, http, vuln]
6
6
  input_types:
7
7
  - host
8
+ - cidr_range
8
9
  tasks:
9
10
  naabu:
10
11
  description: Find open ports
12
+ ports: "-" # scan all ports
11
13
  nmap:
12
14
  description: Search for vulnerabilities on open ports
15
+ skip_host_discovery: True
16
+ version_detection: True
13
17
  targets_: port.host
14
18
  ports_: port.port
15
19
  _group:
@@ -18,17 +22,17 @@ tasks:
18
22
  targets_:
19
23
  - type: port
20
24
  field: '{host}~{service_name}'
21
- condition: item._source == 'nmap' and len(item.service_name.split('/')) > 1
25
+ condition: item._source.startswith('nmap') and len(item.service_name.split('/')) > 1
22
26
  httpx:
23
27
  description: Probe HTTP services on open ports
24
28
  targets_:
25
29
  - type: port
26
30
  field: '{host}:{port}'
27
- condition: item._source == 'nmap'
31
+ condition: item._source.startswith('nmap')
28
32
  results:
29
33
  - type: port
30
34
 
31
35
  - type: url
32
36
  condition: item.status_code != 0
33
37
 
34
- - type: vulnerability
38
+ - type: vulnerability
@@ -13,12 +13,12 @@ tasks:
13
13
  # input: vhost
14
14
  # domain_:
15
15
  # - target.name
16
- # wordlist: /usr/share/seclists/Discovery/DNS/combined_subdomains.txt
16
+ # wordlist: combined_subdomains
17
17
  # gobuster:
18
18
  # input: dns
19
19
  # domain_:
20
20
  # - target.name
21
- # wordlist: /usr/share/seclists/Discovery/DNS/combined_subdomains.txt
21
+ # wordlist: combined_subdomains
22
22
  _group:
23
23
  nuclei:
24
24
  description: Check for subdomain takeovers
@@ -0,0 +1,10 @@
1
+ type: workflow
2
+ name: url_bypass
3
+ alias: urlbypass
4
+ description: Try bypass techniques for 4xx URLs
5
+ tags: [http, crawl]
6
+ input_types:
7
+ - url
8
+ tasks:
9
+ bup:
10
+ description: Bypass 4xx
@@ -8,7 +8,7 @@ input_types:
8
8
  tasks:
9
9
  ffuf:
10
10
  description: Search for HTTP directories
11
- wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt
11
+ wordlist: directory_list_small
12
12
  targets_:
13
13
  - type: target
14
14
  field: '{name}/FUZZ'
@@ -34,7 +34,7 @@ tasks:
34
34
  targets_:
35
35
  - type: tag
36
36
  field: match
37
- condition: item._source == "gf"
37
+ condition: item._source.startswith("gf")
38
38
 
39
39
  # TODO: Add support for SQLMap
40
40
  # sqlmap: