secator 0.10.1a12__py3-none-any.whl → 0.15.1__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 +10 -5
- secator/celery_signals.py +2 -11
- secator/cli.py +309 -69
- secator/config.py +3 -2
- secator/configs/profiles/aggressive.yaml +6 -5
- secator/configs/profiles/default.yaml +6 -7
- secator/configs/profiles/insane.yaml +8 -0
- secator/configs/profiles/paranoid.yaml +8 -0
- secator/configs/profiles/polite.yaml +8 -0
- secator/configs/profiles/sneaky.yaml +8 -0
- secator/configs/profiles/tor.yaml +5 -0
- secator/configs/workflows/host_recon.yaml +11 -2
- secator/configs/workflows/url_dirsearch.yaml +5 -0
- secator/configs/workflows/url_params_fuzz.yaml +25 -0
- secator/configs/workflows/wordpress.yaml +4 -1
- secator/decorators.py +64 -34
- secator/definitions.py +8 -4
- secator/installer.py +84 -49
- secator/output_types/__init__.py +2 -1
- secator/output_types/certificate.py +78 -0
- secator/output_types/stat.py +3 -0
- secator/output_types/user_account.py +1 -1
- secator/report.py +2 -2
- secator/rich.py +1 -1
- secator/runners/_base.py +50 -11
- secator/runners/_helpers.py +15 -3
- secator/runners/command.py +85 -21
- secator/runners/scan.py +6 -3
- secator/runners/task.py +1 -0
- secator/runners/workflow.py +22 -4
- secator/tasks/_categories.py +25 -17
- secator/tasks/arjun.py +92 -0
- secator/tasks/bbot.py +33 -4
- secator/tasks/bup.py +4 -2
- secator/tasks/cariddi.py +17 -4
- secator/tasks/dalfox.py +4 -2
- secator/tasks/dirsearch.py +4 -2
- secator/tasks/dnsx.py +5 -2
- secator/tasks/dnsxbrute.py +4 -1
- secator/tasks/feroxbuster.py +5 -2
- secator/tasks/ffuf.py +7 -3
- secator/tasks/fping.py +4 -1
- secator/tasks/gau.py +5 -2
- secator/tasks/gf.py +4 -2
- secator/tasks/gitleaks.py +79 -0
- secator/tasks/gospider.py +5 -2
- secator/tasks/grype.py +5 -2
- secator/tasks/h8mail.py +4 -2
- secator/tasks/httpx.py +6 -3
- secator/tasks/katana.py +6 -3
- secator/tasks/maigret.py +4 -2
- secator/tasks/mapcidr.py +5 -3
- secator/tasks/msfconsole.py +8 -6
- secator/tasks/naabu.py +16 -5
- secator/tasks/nmap.py +31 -29
- secator/tasks/nuclei.py +18 -10
- secator/tasks/searchsploit.py +8 -3
- secator/tasks/subfinder.py +6 -3
- secator/tasks/testssl.py +276 -0
- secator/tasks/trivy.py +98 -0
- secator/tasks/wafw00f.py +85 -0
- secator/tasks/wpprobe.py +96 -0
- secator/tasks/wpscan.py +8 -4
- secator/template.py +61 -67
- secator/utils.py +31 -18
- secator/utils_test.py +34 -10
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/METADATA +11 -3
- secator-0.15.1.dist-info/RECORD +128 -0
- secator/configs/profiles/stealth.yaml +0 -7
- secator-0.10.1a12.dist-info/RECORD +0 -116
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/WHEEL +0 -0
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/entry_points.txt +0 -0
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/licenses/LICENSE +0 -0
secator/installer.py
CHANGED
|
@@ -11,6 +11,7 @@ import io
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from datetime import datetime
|
|
13
13
|
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
import json
|
|
16
17
|
import requests
|
|
@@ -30,8 +31,10 @@ class InstallerStatus(Enum):
|
|
|
30
31
|
INSTALL_FAILED = 'INSTALL_FAILED'
|
|
31
32
|
INSTALL_NOT_SUPPORTED = 'INSTALL_NOT_SUPPORTED'
|
|
32
33
|
INSTALL_SKIPPED_OK = 'INSTALL_SKIPPED_OK'
|
|
34
|
+
INSTALL_VERSION_NOT_SPECIFIED = 'INSTALL_VERSION_NOT_SPECIFIED'
|
|
33
35
|
GITHUB_LATEST_RELEASE_NOT_FOUND = 'GITHUB_LATEST_RELEASE_NOT_FOUND'
|
|
34
36
|
GITHUB_RELEASE_NOT_FOUND = 'RELEASE_NOT_FOUND'
|
|
37
|
+
GITHUB_RELEASE_UNMATCHED_DISTRIBUTION = 'RELEASE_UNMATCHED_DISTRIBUTION'
|
|
35
38
|
GITHUB_RELEASE_FAILED_DOWNLOAD = 'GITHUB_RELEASE_FAILED_DOWNLOAD'
|
|
36
39
|
GITHUB_BINARY_NOT_FOUND_IN_ARCHIVE = 'GITHUB_BINARY_NOT_FOUND_IN_ARCHIVE'
|
|
37
40
|
UNKNOWN_DISTRIBUTION = 'UNKNOWN_DISTRIBUTION'
|
|
@@ -44,6 +47,7 @@ class InstallerStatus(Enum):
|
|
|
44
47
|
@dataclass
|
|
45
48
|
class Distribution:
|
|
46
49
|
name: str
|
|
50
|
+
system: str
|
|
47
51
|
pm_name: str
|
|
48
52
|
pm_installer: str
|
|
49
53
|
pm_finalizer: str
|
|
@@ -82,12 +86,12 @@ class ToolInstaller:
|
|
|
82
86
|
# Install binaries from GH
|
|
83
87
|
gh_status = InstallerStatus.UNKNOWN
|
|
84
88
|
if tool_cls.install_github_handle and not CONFIG.security.force_source_install:
|
|
85
|
-
gh_status = GithubInstaller.install(tool_cls.install_github_handle)
|
|
89
|
+
gh_status = GithubInstaller.install(tool_cls.install_github_handle, version=tool_cls.install_version or 'latest')
|
|
86
90
|
status = gh_status
|
|
87
91
|
|
|
88
92
|
# Install from source
|
|
89
93
|
if tool_cls.install_cmd and not gh_status.is_ok():
|
|
90
|
-
status = SourceInstaller.install(tool_cls.install_cmd)
|
|
94
|
+
status = SourceInstaller.install(tool_cls.install_cmd, tool_cls.install_version)
|
|
91
95
|
if not status.is_ok():
|
|
92
96
|
cls.print_status(status, name)
|
|
93
97
|
return status
|
|
@@ -166,12 +170,14 @@ class SourceInstaller:
|
|
|
166
170
|
"""Install a tool from source."""
|
|
167
171
|
|
|
168
172
|
@classmethod
|
|
169
|
-
def install(cls, config, install_prereqs=True):
|
|
173
|
+
def install(cls, config, version=None, install_prereqs=True):
|
|
170
174
|
"""Install from source.
|
|
171
175
|
|
|
172
176
|
Args:
|
|
173
177
|
cls: ToolInstaller class.
|
|
174
178
|
config (dict): A dict of distros as keys and a command as value.
|
|
179
|
+
version (str, optional): Version to install.
|
|
180
|
+
install_prereqs (bool, optional): Install pre-requisites.
|
|
175
181
|
|
|
176
182
|
Returns:
|
|
177
183
|
Status: install status.
|
|
@@ -181,6 +187,8 @@ class SourceInstaller:
|
|
|
181
187
|
install_cmd = config
|
|
182
188
|
else:
|
|
183
189
|
distribution = get_distro_config()
|
|
190
|
+
if not distribution.pm_installer:
|
|
191
|
+
return InstallerStatus.UNKNOWN_DISTRIBUTION
|
|
184
192
|
for distros, command in config.items():
|
|
185
193
|
if distribution.name in distros.split("|") or distros == '*':
|
|
186
194
|
install_cmd = command
|
|
@@ -203,6 +211,11 @@ class SourceInstaller:
|
|
|
203
211
|
if not status.is_ok():
|
|
204
212
|
return status
|
|
205
213
|
|
|
214
|
+
# Handle version
|
|
215
|
+
if '[install_version]' in install_cmd:
|
|
216
|
+
version = version or 'latest'
|
|
217
|
+
install_cmd = install_cmd.replace('[install_version]', version)
|
|
218
|
+
|
|
206
219
|
# Run command
|
|
207
220
|
ret = Command.execute(install_cmd, cls_attributes={'shell': True}, quiet=False)
|
|
208
221
|
return InstallerStatus.SUCCESS if ret.return_code == 0 else InstallerStatus.INSTALL_FAILED
|
|
@@ -212,7 +225,7 @@ class GithubInstaller:
|
|
|
212
225
|
"""Install a tool from GitHub releases."""
|
|
213
226
|
|
|
214
227
|
@classmethod
|
|
215
|
-
def install(cls, github_handle):
|
|
228
|
+
def install(cls, github_handle, version='latest'):
|
|
216
229
|
"""Find and install a release from a GitHub handle {user}/{repo}.
|
|
217
230
|
|
|
218
231
|
Args:
|
|
@@ -222,35 +235,38 @@ class GithubInstaller:
|
|
|
222
235
|
InstallerStatus: status.
|
|
223
236
|
"""
|
|
224
237
|
_, repo = tuple(github_handle.split('/'))
|
|
225
|
-
|
|
226
|
-
if not
|
|
227
|
-
return InstallerStatus.
|
|
238
|
+
release = cls.get_release(github_handle, version=version)
|
|
239
|
+
if not release:
|
|
240
|
+
return InstallerStatus.GITHUB_RELEASE_NOT_FOUND
|
|
228
241
|
|
|
229
242
|
# Find the right asset to download
|
|
230
|
-
os_identifiers, arch_identifiers = cls._get_platform_identifier()
|
|
231
|
-
download_url = cls._find_matching_asset(
|
|
243
|
+
system, arch, os_identifiers, arch_identifiers = cls._get_platform_identifier()
|
|
244
|
+
download_url = cls._find_matching_asset(release['assets'], os_identifiers, arch_identifiers)
|
|
232
245
|
if not download_url:
|
|
233
|
-
console.print(Error(message='Could not find a GitHub release matching distribution.'))
|
|
234
|
-
return InstallerStatus.
|
|
246
|
+
console.print(Error(message=f'Could not find a GitHub release matching distribution (system: {system}, arch: {arch}).')) # noqa: E501
|
|
247
|
+
return InstallerStatus.GITHUB_RELEASE_UNMATCHED_DISTRIBUTION
|
|
235
248
|
|
|
236
249
|
# Download and unpack asset
|
|
237
250
|
console.print(Info(message=f'Found release URL: {download_url}'))
|
|
238
251
|
return cls._download_and_unpack(download_url, CONFIG.dirs.bin, repo)
|
|
239
252
|
|
|
240
253
|
@classmethod
|
|
241
|
-
def
|
|
242
|
-
"""Get
|
|
254
|
+
def get_release(cls, github_handle, version='latest'):
|
|
255
|
+
"""Get release from GitHub.
|
|
243
256
|
|
|
244
257
|
Args:
|
|
245
258
|
github_handle (str): A GitHub handle {user}/{repo}.
|
|
246
259
|
|
|
247
260
|
Returns:
|
|
248
|
-
dict:
|
|
261
|
+
dict: Release JSON from GitHub releases.
|
|
249
262
|
"""
|
|
250
263
|
if not github_handle:
|
|
251
264
|
return False
|
|
252
265
|
owner, repo = tuple(github_handle.split('/'))
|
|
253
|
-
|
|
266
|
+
if version == 'latest':
|
|
267
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
|
|
268
|
+
else:
|
|
269
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{version}"
|
|
254
270
|
headers = {}
|
|
255
271
|
if CONFIG.cli.github_token:
|
|
256
272
|
headers['Authorization'] = f'Bearer {CONFIG.cli.github_token}'
|
|
@@ -261,11 +277,13 @@ class GithubInstaller:
|
|
|
261
277
|
return latest_release
|
|
262
278
|
except requests.RequestException as e:
|
|
263
279
|
console.print(Warning(message=f'Failed to fetch latest release for {github_handle}: {str(e)}'))
|
|
280
|
+
if 'rate limit exceeded' in str(e):
|
|
281
|
+
console.print(Warning(message='Consider setting env variable SECATOR_CLI_GITHUB_TOKEN or use secator config set cli.github_token $TOKEN.')) # noqa: E501
|
|
264
282
|
return None
|
|
265
283
|
|
|
266
284
|
@classmethod
|
|
267
285
|
def get_latest_version(cls, github_handle):
|
|
268
|
-
latest_release = cls.
|
|
286
|
+
latest_release = cls.get_release(github_handle, version='latest')
|
|
269
287
|
if not latest_release:
|
|
270
288
|
return None
|
|
271
289
|
return latest_release['tag_name'].lstrip('v')
|
|
@@ -285,16 +303,16 @@ class GithubInstaller:
|
|
|
285
303
|
|
|
286
304
|
# Enhanced architecture mapping to avoid conflicts
|
|
287
305
|
arch_mapping = {
|
|
288
|
-
'x86_64': ['amd64', 'x86_64'],
|
|
289
|
-
'amd64': ['amd64', 'x86_64'],
|
|
306
|
+
'x86_64': ['amd64', 'x86_64', '64bit', 'x64'],
|
|
307
|
+
'amd64': ['amd64', 'x86_64', '64bit', 'x64'],
|
|
290
308
|
'aarch64': ['arm64', 'aarch64'],
|
|
291
309
|
'armv7l': ['armv7', 'arm'],
|
|
292
|
-
'386': ['386', 'x86', 'i386'],
|
|
310
|
+
'386': ['386', 'x86', 'i386', '32bit', 'x32'],
|
|
293
311
|
}
|
|
294
312
|
|
|
295
313
|
os_identifiers = os_mapping.get(system, [])
|
|
296
314
|
arch_identifiers = arch_mapping.get(arch, [])
|
|
297
|
-
return os_identifiers, arch_identifiers
|
|
315
|
+
return system, arch, os_identifiers, arch_identifiers
|
|
298
316
|
|
|
299
317
|
@classmethod
|
|
300
318
|
def _find_matching_asset(cls, assets, os_identifiers, arch_identifiers):
|
|
@@ -348,6 +366,9 @@ class GithubInstaller:
|
|
|
348
366
|
elif url.endswith('.tar.gz'):
|
|
349
367
|
with tarfile.open(fileobj=io.BytesIO(response.content), mode='r:gz') as tar:
|
|
350
368
|
tar.extractall(path=temp_dir)
|
|
369
|
+
else:
|
|
370
|
+
with Path(f'{temp_dir}/{repo_name}').open('wb') as f:
|
|
371
|
+
f.write(response.content)
|
|
351
372
|
|
|
352
373
|
# For archives, find and move the binary that matches the repo name
|
|
353
374
|
binary_path = cls._find_binary_in_directory(temp_dir, repo_name)
|
|
@@ -397,13 +418,11 @@ def get_version(version_cmd):
|
|
|
397
418
|
import re
|
|
398
419
|
regex = r'[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*'
|
|
399
420
|
ret = Command.execute(version_cmd, quiet=True, print_errors=False)
|
|
400
|
-
return_code = ret.return_code
|
|
401
|
-
if not return_code == 0:
|
|
402
|
-
return '', ret.return_code
|
|
403
421
|
match = re.findall(regex, ret.output)
|
|
404
422
|
if not match:
|
|
405
|
-
|
|
406
|
-
|
|
423
|
+
console.print(Warning(message=f'Failed to find version in version command output. Command: {version_cmd}; Output: {ret.output}; Return code: {ret.return_code}')) # noqa: E501
|
|
424
|
+
return None
|
|
425
|
+
return match[0]
|
|
407
426
|
|
|
408
427
|
|
|
409
428
|
def parse_version(ver):
|
|
@@ -436,14 +455,22 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
|
|
|
436
455
|
'name': name,
|
|
437
456
|
'installed': False,
|
|
438
457
|
'version': version,
|
|
458
|
+
'version_cmd': None,
|
|
439
459
|
'latest_version': None,
|
|
440
460
|
'location': None,
|
|
441
|
-
'status': ''
|
|
461
|
+
'status': '',
|
|
462
|
+
'outdated': False,
|
|
463
|
+
'errors': []
|
|
442
464
|
}
|
|
443
465
|
|
|
444
466
|
# Get binary path
|
|
445
467
|
location = which(name).output
|
|
468
|
+
if not location or not Path(location).exists():
|
|
469
|
+
info['installed'] = False
|
|
470
|
+
info['status'] = 'missing'
|
|
471
|
+
return info
|
|
446
472
|
info['location'] = location
|
|
473
|
+
info['installed'] = True
|
|
447
474
|
|
|
448
475
|
# Get latest version
|
|
449
476
|
latest_version = None
|
|
@@ -472,34 +499,36 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
|
|
|
472
499
|
if ver:
|
|
473
500
|
latest_version = str(ver)
|
|
474
501
|
info['latest_version'] = latest_version
|
|
502
|
+
else:
|
|
503
|
+
error = f'Failed to get latest version for {name}. Command: apt-cache madison {name}'
|
|
504
|
+
info['errors'].append(error)
|
|
505
|
+
console.print(Warning(message=error))
|
|
475
506
|
|
|
476
507
|
# Get current version
|
|
477
|
-
version_ret = 1
|
|
478
508
|
version_flag = None if version_flag == OPT_NOT_SUPPORTED else version_flag
|
|
479
509
|
if version_flag:
|
|
480
510
|
version_cmd = f'{name} {version_flag}'
|
|
481
|
-
|
|
511
|
+
info['version_cmd'] = version_cmd
|
|
512
|
+
version = get_version(version_cmd)
|
|
482
513
|
info['version'] = version
|
|
483
|
-
if
|
|
484
|
-
info['
|
|
485
|
-
info['status'] = '
|
|
514
|
+
if not version:
|
|
515
|
+
info['errors'].append(f'Error fetching version for command. Version command: {version_cmd}')
|
|
516
|
+
info['status'] = 'version fetch error'
|
|
486
517
|
return info
|
|
487
518
|
|
|
488
|
-
if
|
|
489
|
-
|
|
490
|
-
if version
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
else:
|
|
502
|
-
info['status'] = 'missing'
|
|
519
|
+
# Check if up-to-date
|
|
520
|
+
if version and latest_version:
|
|
521
|
+
if parse_version(version) < parse_version(latest_version):
|
|
522
|
+
info['status'] = 'outdated'
|
|
523
|
+
info['outdated'] = True
|
|
524
|
+
else:
|
|
525
|
+
info['status'] = 'latest'
|
|
526
|
+
elif not version:
|
|
527
|
+
info['status'] = 'current unknown'
|
|
528
|
+
elif not latest_version:
|
|
529
|
+
info['status'] = 'latest unknown'
|
|
530
|
+
if CONFIG.offline_mode:
|
|
531
|
+
info['status'] += r' [dim orange1]\[offline][/]'
|
|
503
532
|
|
|
504
533
|
return info
|
|
505
534
|
|
|
@@ -517,7 +546,7 @@ def get_distro_config():
|
|
|
517
546
|
distrib = system
|
|
518
547
|
|
|
519
548
|
if system == "Linux":
|
|
520
|
-
distrib = distro.id()
|
|
549
|
+
distrib = distro.like() or distro.id()
|
|
521
550
|
|
|
522
551
|
if distrib in ["ubuntu", "debian", "linuxmint", "popos", "kali"]:
|
|
523
552
|
installer = "apt install -y --no-install-recommends"
|
|
@@ -547,12 +576,16 @@ def get_distro_config():
|
|
|
547
576
|
else:
|
|
548
577
|
installer = "scoop" # Alternative package manager for Windows
|
|
549
578
|
|
|
550
|
-
|
|
579
|
+
if not installer:
|
|
580
|
+
console.print(Error(message=f'Could not find installer for your distribution (system: {system}, distrib: {distrib})')) # noqa: E501
|
|
581
|
+
|
|
582
|
+
manager = installer.split(' ')[0] if installer else ''
|
|
551
583
|
config = Distribution(
|
|
552
584
|
pm_installer=installer,
|
|
553
585
|
pm_finalizer=finalizer,
|
|
554
586
|
pm_name=manager,
|
|
555
|
-
name=distrib
|
|
587
|
+
name=distrib,
|
|
588
|
+
system=system
|
|
556
589
|
)
|
|
557
590
|
return config
|
|
558
591
|
|
|
@@ -578,6 +611,8 @@ def fmt_health_table_row(version_info, category=None):
|
|
|
578
611
|
_version = '[bold red]missing[/]'
|
|
579
612
|
elif status == 'ok':
|
|
580
613
|
_version = '[bold green]ok [/]'
|
|
614
|
+
elif status == 'version fetch error':
|
|
615
|
+
_version = '[bold orange1]unknown[/] [dim](current unknown)[/]'
|
|
581
616
|
elif status:
|
|
582
617
|
if not version and installed:
|
|
583
618
|
_version = '[bold green]ok [/]'
|
secator/output_types/__init__.py
CHANGED
|
@@ -26,6 +26,7 @@ from secator.output_types.url import Url
|
|
|
26
26
|
from secator.output_types.user_account import UserAccount
|
|
27
27
|
from secator.output_types.vulnerability import Vulnerability
|
|
28
28
|
from secator.output_types.record import Record
|
|
29
|
+
from secator.output_types.certificate import Certificate
|
|
29
30
|
from secator.output_types.info import Info
|
|
30
31
|
from secator.output_types.warning import Warning
|
|
31
32
|
from secator.output_types.error import Error
|
|
@@ -39,6 +40,6 @@ STAT_TYPES = [
|
|
|
39
40
|
Stat
|
|
40
41
|
]
|
|
41
42
|
FINDING_TYPES = [
|
|
42
|
-
Subdomain, Ip, Port, Url, Tag, Exploit, UserAccount, Vulnerability
|
|
43
|
+
Subdomain, Ip, Port, Url, Tag, Exploit, UserAccount, Vulnerability, Certificate
|
|
43
44
|
]
|
|
44
45
|
OUTPUT_TYPES = FINDING_TYPES + EXECUTION_TYPES + STAT_TYPES
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from secator.output_types import OutputType
|
|
5
|
+
from secator.utils import rich_to_ansi
|
|
6
|
+
from secator.definitions import CERTIFICATE_STATUS_UNKNOWN
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Certificate(OutputType):
|
|
11
|
+
host: str
|
|
12
|
+
fingerprint_sha256: str = field(default='')
|
|
13
|
+
ip: str = field(default='', compare=False)
|
|
14
|
+
raw_value: str = field(default='', compare=False)
|
|
15
|
+
subject_cn: str = field(default='', compare=False)
|
|
16
|
+
subject_an: list[str] = field(default_factory=list, compare=False)
|
|
17
|
+
not_before: datetime = field(default=None, compare=False)
|
|
18
|
+
not_after: datetime = field(default=None, compare=False)
|
|
19
|
+
issuer_dn: str = field(default='', compare=False)
|
|
20
|
+
issuer_cn: str = field(default='', compare=False)
|
|
21
|
+
issuer: str = field(default='', compare=False)
|
|
22
|
+
self_signed: bool = field(default=True, compare=False)
|
|
23
|
+
trusted: bool = field(default=False, compare=False)
|
|
24
|
+
status: str = field(default=CERTIFICATE_STATUS_UNKNOWN, compare=False)
|
|
25
|
+
keysize: int = field(default=None, compare=False)
|
|
26
|
+
serial_number: str = field(default='', compare=False)
|
|
27
|
+
ciphers: list[str] = field(default_factory=list, compare=False)
|
|
28
|
+
# parent_certificate: 'Certificate' = None # noqa: F821
|
|
29
|
+
_source: str = field(default='', repr=True)
|
|
30
|
+
_type: str = field(default='certificate', repr=True)
|
|
31
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
32
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
33
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
34
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
35
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
36
|
+
_related: list = field(default_factory=list, compare=False)
|
|
37
|
+
_table_fields = ['ip', 'host']
|
|
38
|
+
_sort_by = ('ip',)
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return self.subject_cn
|
|
42
|
+
|
|
43
|
+
def is_expired(self) -> bool:
|
|
44
|
+
if self.not_after:
|
|
45
|
+
return self.not_after < datetime.now()
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
def is_expired_soon(self, months: int = 1) -> bool:
|
|
49
|
+
if self.not_after:
|
|
50
|
+
return self.not_after < datetime.now() + timedelta(days=months * 30)
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def format_date(date):
|
|
55
|
+
if date:
|
|
56
|
+
return date.strftime("%m/%d/%Y")
|
|
57
|
+
return '?'
|
|
58
|
+
|
|
59
|
+
def __repr__(self) -> str:
|
|
60
|
+
s = f'📜 [bold white]{self.host}[/]'
|
|
61
|
+
s += f' [cyan]{self.status}[/]'
|
|
62
|
+
s += rf' [white]\[fingerprint={self.fingerprint_sha256[:10]}][/]'
|
|
63
|
+
if self.subject_cn:
|
|
64
|
+
s += rf' [white]\[cn={self.subject_cn}][/]'
|
|
65
|
+
if self.subject_an:
|
|
66
|
+
s += rf' [white]\[an={", ".join(self.subject_an)}][/]'
|
|
67
|
+
if self.issuer:
|
|
68
|
+
s += rf' [white]\[issuer={self.issuer}][/]'
|
|
69
|
+
elif self.issuer_cn:
|
|
70
|
+
s += rf' [white]\[issuer_cn={self.issuer_cn}][/]'
|
|
71
|
+
expiry_date = Certificate.format_date(self.not_after)
|
|
72
|
+
if self.is_expired():
|
|
73
|
+
s += f' [red]expired since {expiry_date}[/red]'
|
|
74
|
+
elif self.is_expired_soon(months=2):
|
|
75
|
+
s += f' [yellow]expires <2 months[/yellow], [yellow]valid until {expiry_date}[/yellow]'
|
|
76
|
+
else:
|
|
77
|
+
s += f' [green]not expired[/green], [yellow]valid until {expiry_date}[/yellow]'
|
|
78
|
+
return rich_to_ansi(s)
|
secator/output_types/stat.py
CHANGED
|
@@ -25,6 +25,9 @@ class Stat(OutputType):
|
|
|
25
25
|
_table_fields = ['name', 'pid', 'cpu', 'memory']
|
|
26
26
|
_sort_by = ('name', 'pid')
|
|
27
27
|
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return f'{self.name} [pid={self.pid}] [cpu={self.cpu:.2f}%] [memory={self.memory:.2f}%]'
|
|
30
|
+
|
|
28
31
|
def __repr__(self) -> str:
|
|
29
32
|
s = rf'[dim yellow3]📊 {self.name} \[pid={self.pid}] \[cpu={self.cpu:.2f}%] \[memory={self.memory:.2f}%]'
|
|
30
33
|
if self.net_conns:
|
|
@@ -37,5 +37,5 @@ class UserAccount(OutputType):
|
|
|
37
37
|
if self.url:
|
|
38
38
|
s += rf' \[[white]{_s(self.url)}[/]]'
|
|
39
39
|
if self.extra_data:
|
|
40
|
-
s += r' \[[bold yellow]' + _s(', '.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)
|
secator/report.py
CHANGED
|
@@ -55,7 +55,7 @@ class Report:
|
|
|
55
55
|
f'{str(e)}[/]\n[dim]{traceback_as_string(e)}[/]',
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
-
def build(self, extractors=[], dedupe=
|
|
58
|
+
def build(self, extractors=[], dedupe=CONFIG.runners.remove_duplicates):
|
|
59
59
|
# Trim options
|
|
60
60
|
from secator.decorators import DEFAULT_CLI_OPTIONS
|
|
61
61
|
opts = merge_opts(self.runner.config.options, self.runner.run_opts)
|
|
@@ -97,7 +97,7 @@ class Report:
|
|
|
97
97
|
if items:
|
|
98
98
|
if sort_by and all(sort_by):
|
|
99
99
|
items = sorted(items, key=operator.attrgetter(*sort_by))
|
|
100
|
-
if dedupe
|
|
100
|
+
if dedupe:
|
|
101
101
|
items = remove_duplicates(items)
|
|
102
102
|
# items = [item for item in items if not item._duplicate and item not in dedupe_from]
|
|
103
103
|
for extractor in extractors:
|
secator/rich.py
CHANGED
|
@@ -4,7 +4,7 @@ import yaml
|
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.table import Table
|
|
6
6
|
|
|
7
|
-
console = Console(stderr=True)
|
|
7
|
+
console = Console(stderr=True, record=True)
|
|
8
8
|
console_stdout = Console(record=True)
|
|
9
9
|
# handler = RichHandler(rich_tracebacks=True) # TODO: add logging handler
|
|
10
10
|
|
secator/runners/_base.py
CHANGED
|
@@ -11,7 +11,7 @@ import humanize
|
|
|
11
11
|
from secator.definitions import ADDONS_ENABLED
|
|
12
12
|
from secator.celery_utils import CeleryData
|
|
13
13
|
from secator.config import CONFIG
|
|
14
|
-
from secator.output_types import FINDING_TYPES, OutputType, Progress, Info, Warning, Error, Target, State
|
|
14
|
+
from secator.output_types import FINDING_TYPES, OUTPUT_TYPES, OutputType, Progress, Info, Warning, Error, Target, State
|
|
15
15
|
from secator.report import Report
|
|
16
16
|
from secator.rich import console, console_stdout
|
|
17
17
|
from secator.runners._helpers import (get_task_folder_id, run_extractors)
|
|
@@ -53,7 +53,7 @@ class Runner:
|
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
55
|
# Input field (mostly for tests and CLI)
|
|
56
|
-
|
|
56
|
+
input_types = []
|
|
57
57
|
|
|
58
58
|
# Output types
|
|
59
59
|
output_types = []
|
|
@@ -102,6 +102,7 @@ class Runner:
|
|
|
102
102
|
self.piped_input = self.run_opts.get('piped_input', False)
|
|
103
103
|
self.piped_output = self.run_opts.get('piped_output', False)
|
|
104
104
|
self.enable_duplicate_check = self.run_opts.get('enable_duplicate_check', True)
|
|
105
|
+
self.dry_run = self.run_opts.get('dry_run', False)
|
|
105
106
|
|
|
106
107
|
# Runner print opts
|
|
107
108
|
self.print_item = self.run_opts.get('print_item', False)
|
|
@@ -128,18 +129,19 @@ class Runner:
|
|
|
128
129
|
[self.add_result(result, print=False, output=False) for result in results]
|
|
129
130
|
|
|
130
131
|
# Determine inputs
|
|
131
|
-
inputs = [inputs] if not isinstance(inputs, list) else inputs
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
for error in errors:
|
|
135
|
-
self.add_result(error, print=True)
|
|
136
|
-
self.inputs = list(set(inputs))
|
|
132
|
+
self.inputs = [inputs] if not isinstance(inputs, list) else inputs
|
|
133
|
+
targets = [Target(name=target) for target in self.inputs]
|
|
134
|
+
self.filter_results(results + targets)
|
|
137
135
|
|
|
138
136
|
# Debug
|
|
139
137
|
self.debug('Inputs', obj=self.inputs, sub='init')
|
|
140
138
|
self.debug('Run opts', obj={k: v for k, v in self.run_opts.items() if v is not None}, sub='init')
|
|
141
139
|
self.debug('Print opts', obj={k: v for k, v in self.print_opts.items() if v is not None}, sub='init')
|
|
142
140
|
|
|
141
|
+
# Load profiles
|
|
142
|
+
profiles_str = run_opts.get('profiles', [])
|
|
143
|
+
self.load_profiles(profiles_str)
|
|
144
|
+
|
|
143
145
|
# Determine exporters
|
|
144
146
|
exporters_str = self.run_opts.get('output') or self.default_exporters
|
|
145
147
|
self.exporters = self.resolve_exporters(exporters_str)
|
|
@@ -310,6 +312,8 @@ class Runner:
|
|
|
310
312
|
self.mark_completed()
|
|
311
313
|
|
|
312
314
|
finally:
|
|
315
|
+
if self.dry_run:
|
|
316
|
+
return
|
|
313
317
|
if self.sync:
|
|
314
318
|
self.mark_completed()
|
|
315
319
|
if self.enable_reports:
|
|
@@ -328,6 +332,15 @@ class Runner:
|
|
|
328
332
|
self.add_result(error, print=True)
|
|
329
333
|
yield error
|
|
330
334
|
|
|
335
|
+
def filter_results(self, results):
|
|
336
|
+
"""Filter results based on the runner's config."""
|
|
337
|
+
if not self.chunk:
|
|
338
|
+
inputs, run_opts, errors = run_extractors(results, self.run_opts, self.inputs, self.dry_run)
|
|
339
|
+
for error in errors:
|
|
340
|
+
self.add_result(error, print=True)
|
|
341
|
+
self.inputs = list(set(inputs))
|
|
342
|
+
self.run_opts = run_opts
|
|
343
|
+
|
|
331
344
|
def add_result(self, item, print=False, output=True):
|
|
332
345
|
"""Add item to runner results.
|
|
333
346
|
|
|
@@ -818,7 +831,7 @@ class Runner:
|
|
|
818
831
|
if isinstance(data, (OutputType, dict)):
|
|
819
832
|
if getattr(data, 'toDict', None):
|
|
820
833
|
data = data.toDict()
|
|
821
|
-
data = json.dumps(data)
|
|
834
|
+
data = json.dumps(data, default=str)
|
|
822
835
|
print(data, file=out)
|
|
823
836
|
|
|
824
837
|
def _get_findings_count(self):
|
|
@@ -911,8 +924,8 @@ class Runner:
|
|
|
911
924
|
elif isinstance(item, Info) and item.task_id and item.task_id not in self.celery_ids:
|
|
912
925
|
self.celery_ids.append(item.task_id)
|
|
913
926
|
|
|
914
|
-
# If
|
|
915
|
-
elif isinstance(item, tuple(
|
|
927
|
+
# If output type, run on_item hooks
|
|
928
|
+
elif isinstance(item, tuple(OUTPUT_TYPES)):
|
|
916
929
|
item = self.run_hooks('on_item', item)
|
|
917
930
|
if not item:
|
|
918
931
|
return
|
|
@@ -944,6 +957,32 @@ class Runner:
|
|
|
944
957
|
]
|
|
945
958
|
return [cls for cls in classes if cls]
|
|
946
959
|
|
|
960
|
+
def load_profiles(self, profiles):
|
|
961
|
+
"""Load profiles and update run options.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
profiles (list[str]): List of profile names to resolve.
|
|
965
|
+
|
|
966
|
+
Returns:
|
|
967
|
+
list: List of profiles.
|
|
968
|
+
"""
|
|
969
|
+
from secator.cli import ALL_PROFILES
|
|
970
|
+
if isinstance(profiles, str):
|
|
971
|
+
profiles = profiles.split(',')
|
|
972
|
+
templates = []
|
|
973
|
+
for pname in profiles:
|
|
974
|
+
matches = [p for p in ALL_PROFILES if p.name == pname]
|
|
975
|
+
if not matches:
|
|
976
|
+
self._print(Warning(message=f'Profile "{pname}" was not found'), rich=True)
|
|
977
|
+
else:
|
|
978
|
+
templates.append(matches[0])
|
|
979
|
+
opts = {}
|
|
980
|
+
for profile in templates:
|
|
981
|
+
self._print(Info(message=f'Loaded profile {profile.name} ({profile.description})'), rich=True)
|
|
982
|
+
opts.update(profile.opts)
|
|
983
|
+
opts = {k: v for k, v in opts.items() if k not in self.run_opts}
|
|
984
|
+
self.run_opts.update(opts)
|
|
985
|
+
|
|
947
986
|
@classmethod
|
|
948
987
|
def get_func_path(cls, func):
|
|
949
988
|
"""Get the full symbolic path of a function or method, including staticmethods, using function and method
|
secator/runners/_helpers.py
CHANGED
|
@@ -4,27 +4,39 @@ from secator.output_types import Error
|
|
|
4
4
|
from secator.utils import deduplicate, debug
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def run_extractors(results, opts, inputs=[]):
|
|
7
|
+
def run_extractors(results, opts, inputs=[], dry_run=False):
|
|
8
8
|
"""Run extractors and merge extracted values with option dict.
|
|
9
9
|
|
|
10
10
|
Args:
|
|
11
11
|
results (list): List of results.
|
|
12
12
|
opts (dict): Options.
|
|
13
13
|
inputs (list): Original inputs.
|
|
14
|
+
dry_run (bool): Dry run.
|
|
14
15
|
|
|
15
16
|
Returns:
|
|
16
17
|
tuple: inputs, options, errors.
|
|
17
18
|
"""
|
|
18
19
|
extractors = {k: v for k, v in opts.items() if k.endswith('_')}
|
|
19
20
|
errors = []
|
|
21
|
+
computed_inputs = []
|
|
22
|
+
computed_opts = {}
|
|
20
23
|
for key, val in extractors.items():
|
|
21
24
|
key = key.rstrip('_')
|
|
22
25
|
values, err = extract_from_results(results, val)
|
|
23
26
|
errors.extend(err)
|
|
24
27
|
if key == 'targets':
|
|
25
|
-
|
|
28
|
+
targets = ['<COMPUTED>'] if dry_run else deduplicate(values)
|
|
29
|
+
computed_inputs.extend(targets)
|
|
26
30
|
else:
|
|
27
|
-
|
|
31
|
+
computed_opt = ['<COMPUTED>'] if dry_run else deduplicate(values)
|
|
32
|
+
if computed_opt:
|
|
33
|
+
computed_opts[key] = computed_opt
|
|
34
|
+
opts[key] = computed_opts[key]
|
|
35
|
+
if computed_inputs:
|
|
36
|
+
debug('computed_inputs', obj=computed_inputs, sub='extractors')
|
|
37
|
+
inputs = computed_inputs
|
|
38
|
+
if computed_opts:
|
|
39
|
+
debug('computed_opts', obj=computed_opts, sub='extractors')
|
|
28
40
|
return inputs, opts, errors
|
|
29
41
|
|
|
30
42
|
|