diff --git a/scripts/install_go.sh b/scripts/install_go.sh index 4551870e..912ca6c3 100755 --- a/scripts/install_go.sh +++ b/scripts/install_go.sh @@ -7,9 +7,6 @@ YELLOW='\033[0;93m' GREEN='\033[0;92m' NC='\033[0m' # No Color -echo -e "🗄 ${YELLOW}Removing existing Go build ...${NC}" -sudo rm -rf /usr/local/go - echo -e "🗄 ${YELLOW}Downloading Go $GO_VERSION ...${NC}" wget https://golang.org/dl/$GO_TAR @@ -18,8 +15,8 @@ tar -xvf $GO_TAR rm $GO_TAR || true echo -e "🗄 ${YELLOW}Linking Go install to /usr/local ...${NC}" -sudo mv go /usr/local -sudo rm /usr/bin/go || true -sudo ln -s /usr/local/go/bin/go /usr/bin/go +sudo mv go /usr/local/go$GO_VERSION +sudo mv /usr/bin/go /usr/bin/go.bak || true +sudo ln -s /usr/local/go$GO_VERSION/bin/go /usr/bin/go echo -e "🗄 ${GREEN}Go $GO_VERSION installed successfully !${NC}\n" diff --git a/secator/celery.py b/secator/celery.py index 11362b36..6e0e0407 100644 --- a/secator/celery.py +++ b/secator/celery.py @@ -106,7 +106,7 @@ def void(*args, **kwargs): def revoke_task(task_id): console.print(f'Revoking task {task_id}') - return app.control.revoke(task_id, terminate=True, signal='SIGKILL') + return app.control.revoke(task_id, terminate=True, signal='SIGINT') #--------------# diff --git a/secator/cli.py b/secator/cli.py index 167c88fc..33111020 100644 --- a/secator/cli.py +++ b/secator/cli.py @@ -7,22 +7,20 @@ from dotmap import DotMap from fp.fp import FreeProxy from jinja2 import Template +from rich.live import Live from rich.markdown import Markdown from rich.rule import Rule from secator.config import ConfigLoader from secator.decorators import OrderedGroup, register_runner -from secator.definitions import (ASCII, BUILD_ADDON_ENABLED, CVES_FOLDER, DATA_FOLDER, # noqa: F401 - DEFAULT_DNS_WORDLIST, DEFAULT_DNS_WORDLIST_URL, DEFAULT_HTTP_WORDLIST, - DEFAULT_HTTP_WORDLIST_URL, DEV_ADDON_ENABLED, DEV_PACKAGE, GOOGLE_ADDON_ENABLED, - LIB_FOLDER, MONGODB_ADDON_ENABLED, OPT_NOT_SUPPORTED, PAYLOADS_FOLDER, - REDIS_ADDON_ENABLED, REVSHELLS_FOLDER, ROOT_FOLDER, TRACE_ADDON_ENABLED, VERSION, - VERSION_LATEST, VERSION_OBSOLETE, VERSION_STR, WORKER_ADDON_ENABLED) -from secator.installer import ToolInstaller +from secator.definitions import (ADDONS_ENABLED, ASCII, CVES_FOLDER, DATA_FOLDER, DEV_PACKAGE, OPT_NOT_SUPPORTED, + PAYLOADS_FOLDER, REVSHELLS_FOLDER, ROOT_FOLDER, VERSION) +from secator.installer import ToolInstaller, get_version_info, get_health_table, fmt_health_table_row from secator.rich import console from secator.runners import Command from secator.serializers.dataclass import loads_dataclass -from secator.utils import debug, detect_host, discover_tasks, find_list_item, flatten, print_results_table +from secator.utils import (debug, detect_host, discover_tasks, find_list_item, flatten, + print_results_table, print_version) click.rich_click.USE_RICH_MARKUP = True @@ -32,15 +30,6 @@ ALL_SCANS = ALL_CONFIGS.scan -def print_version(): - console.print(f'[bold gold3]Current version[/]: {VERSION}', highlight=False) - console.print(f'[bold gold3]Latest version[/]: {VERSION_LATEST}', highlight=False) - console.print(f'[bold gold3]Python binary[/]: {sys.executable}') - if DEV_PACKAGE: - console.print(f'[bold gold3]Root folder[/]: {ROOT_FOLDER}') - console.print(f'[bold gold3]Lib folder[/]: {LIB_FOLDER}') - - #-----# # CLI # #-----# @@ -51,10 +40,6 @@ def print_version(): def cli(ctx, version): """Secator CLI.""" console.print(ASCII, highlight=False) - if VERSION_OBSOLETE: - console.print( - '[bold red]:warning: secator version is outdated: ' - f'run "secator update" to install the newest version ({VERSION_LATEST}).\n') if ctx.invoked_subcommand is None: if version: print_version() @@ -121,7 +106,7 @@ def scan(): @click.option('--show', is_flag=True, help='Show command (celery multi).') def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show): """Run a worker.""" - if not WORKER_ADDON_ENABLED: + if not ADDONS_ENABLED['worker']: console.print('[bold red]Missing worker addon: please run `secator install addons worker`[/].') sys.exit(1) from secator.celery import app, is_celery_worker_alive @@ -410,7 +395,7 @@ def build(): @build.command('pypi') def build_pypi(): """Build secator PyPI package.""" - if not BUILD_ADDON_ENABLED: + if not ADDONS_ENABLED['build']: console.print('[bold red]Missing build addon: please run `secator install addons build`') sys.exit(1) with console.status('[bold gold3]Building PyPI package...[/]'): @@ -446,7 +431,7 @@ def publish(): @publish.command('pypi') def publish_pypi(): """Publish secator PyPI package.""" - if not BUILD_ADDON_ENABLED: + if not ADDONS_ENABLED['build']: console.print('[bold red]Missing build addon: please run `secator install addons build`') sys.exit(1) os.environ['HATCH_INDEX_USER'] = '__token__' @@ -528,114 +513,64 @@ def report_show(json_path, exclude_fields): # HEALTH # #--------# - -def which(command): - """Run which on a command. - - Args: - command (str): Command to check. - - Returns: - secator.Command: Command instance. - """ - return Command.execute(f'which {command}', quiet=True, print_errors=False) - - -def get_version_cls(cls): - """Get version for a Command. - - Args: - cls: Command class. - - Returns: - string: Version string or 'n/a' if not found. - """ - base_cmd = cls.cmd.split(' ')[0] - if cls.version_flag == OPT_NOT_SUPPORTED: - return 'N/A' - version_flag = cls.version_flag or f'{cls.opt_prefix}version' - version_cmd = f'{base_cmd} {version_flag}' - return get_version(version_cmd) - - -def get_version(version_cmd): - """Run version command and match first version number found. - - Args: - version_cmd (str): Command to get the version. - - Returns: - str: Version string. - """ - regex = r'[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*' - ret = Command.execute(version_cmd, quiet=True, print_errors=False) - match = re.findall(regex, ret.output) - if not match: - return 'n/a' - return match[0] - - @cli.command(name='health') @click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output') @click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output') def health(json, debug): """[dim]Get health status.[/]""" - tools = [cls for cls in ALL_TASKS] - status = {'tools': {}, 'languages': {}, 'secator': {}} - - def print_status(cmd, return_code, version=None, bin=None, category=None): - s = '[bold green]ok [/]' if return_code == 0 else '[bold red]missing [/]' - s = f'[bold magenta]{cmd:<15}[/] {s} ' - if return_code == 0 and version: - if version == 'N/A': - s += f'[dim blue]{version:<12}[/]' - else: - s += f'[bold blue]{version:<12}[/]' - elif category: - s += ' '*12 + f'[dim]# secator install {category} {cmd}' - if bin: - s += f'[dim gold3]{bin}[/]' - console.print(s, highlight=False) + tools = ALL_TASKS + status = {'secator': {}, 'languages': {}, 'tools': {}, 'addons': {}} # Check secator - if not json: - console.print(':wrench: [bold gold3]Checking secator ...[/]') - ret = which('secator') - if not json: - print_status('secator', ret.return_code, VERSION, ret.output, None) - status['secator'] = {'installed': ret.return_code == 0, 'version': VERSION} + console.print(':wrench: [bold gold3]Checking secator ...[/]') + info = get_version_info('secator', '-version', 'freelabz/secator') + table = get_health_table() + with Live(table, console=console): + row = fmt_health_table_row(info) + table.add_row(*row) + status['secator'] = info # Check languages - if not json: - console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]') + console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]') version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version'} - for lang, version_flag in version_cmds.items(): - ret = which(lang) - version = get_version(f'{lang} {version_flag}') - if not json: - print_status(lang, ret.return_code, version, ret.output, 'langs') - status['languages'][lang] = {'installed': ret.return_code == 0, 'version': version} + table = get_health_table() + with Live(table, console=console): + for lang, version_flag in version_cmds.items(): + info = get_version_info(lang, version_flag) + row = fmt_health_table_row(info, 'langs') + table.add_row(*row) + status['languages'][lang] = info # Check tools - if not json: - console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]') - for tool in tools: - cmd = tool.cmd.split(' ')[0] - ret = which(cmd) - version = get_version_cls(tool) - if not json: - print_status(tool.__name__, ret.return_code, version, ret.output, 'tools') - status['tools'][tool.__name__] = {'installed': ret.return_code == 0, 'version': version} - - # Check addons - if not json: - console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]') - for addon in ['google', 'mongodb', 'redis', 'dev', 'trace', 'build']: - addon_var = globals()[f'{addon.upper()}_ADDON_ENABLED'] - ret = 0 if addon_var == 1 else 1 - bin = None if addon_var == 0 else ' ' - if not json: - print_status(addon, ret, 'N/A', bin, 'addons') + console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]') + table = get_health_table() + with Live(table, console=console): + for tool in tools: + cmd = tool.cmd.split(' ')[0] + version_flag = tool.version_flag or f'{tool.opt_prefix}version' + version_flag = None if tool.version_flag == OPT_NOT_SUPPORTED else version_flag + info = get_version_info(cmd, version_flag, tool.install_github_handle) + row = fmt_health_table_row(info, 'tools') + table.add_row(*row) + status['tools'][tool.__name__] = info + + # # Check addons + console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]') + table = get_health_table() + with Live(table, console=console): + for addon in ['google', 'mongodb', 'redis', 'dev', 'trace', 'build']: + addon_var = ADDONS_ENABLED[addon] + info = { + 'name': addon, + 'version': None, + 'status': 'ok' if addon_var else 'missing', + 'latest_version': None, + 'installed': addon_var, + 'location': None + } + row = fmt_health_table_row(info, 'addons') + table.add_row(*row) + status['addons'][addon] = info # Print JSON health if json: @@ -844,13 +779,16 @@ def install_cves(force): @cli.command('update') def update(): """[dim]Update to latest version.[/]""" - if not VERSION_OBSOLETE: - console.print(f'[bold green]secator is already at the newest version {VERSION_LATEST}[/]') - console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {VERSION_LATEST} ...[/]') + info = get_version_info('secator', github_handle='freelabz/secator', version=VERSION) + latest_version = info['latest_version'] + if info['status'] == 'latest': + console.print(f'[bold green]secator is already at the newest version {latest_version}[/] !') + sys.exit(0) + console.print(f'[bold gold3]:wrench: Updating secator from {VERSION} to {latest_version} ...[/]') if 'pipx' in sys.executable: - Command.execute(f'pipx install secator=={VERSION_LATEST} --force') + Command.execute(f'pipx install secator=={latest_version} --force') else: - Command.execute(f'pip install secator=={VERSION_LATEST}') + Command.execute(f'pip install secator=={latest_version}') #-------# @@ -955,7 +893,7 @@ def test(): if not DEV_PACKAGE: console.print('[bold red]You MUST use a development version of secator to run tests.[/]') sys.exit(1) - if not DEV_ADDON_ENABLED: + if not ADDONS_ENABLED['dev']: console.print('[bold red]Missing dev addon: please run `secator install addons dev`') sys.exit(1) pass diff --git a/secator/decorators.py b/secator/decorators.py index 02304f63..e35565fb 100644 --- a/secator/decorators.py +++ b/secator/decorators.py @@ -5,8 +5,7 @@ from rich_click.rich_click import _get_rich_console from rich_click.rich_group import RichGroup -from secator.definitions import (MONGODB_ADDON_ENABLED, OPT_NOT_SUPPORTED, - WORKER_ADDON_ENABLED) +from secator.definitions import ADDONS_ENABLED, OPT_NOT_SUPPORTED from secator.runners import Scan, Task, Workflow from secator.utils import (deduplicate, expand_input, get_command_category, get_command_cls) @@ -276,7 +275,7 @@ def func(ctx, **opts): # opts.update(unknown_opts) targets = opts.pop(input_type) targets = expand_input(targets) - if sync or show or not WORKER_ADDON_ENABLED: + if sync or show or not ADDONS_ENABLED['worker']: sync = True elif worker: sync = False @@ -294,7 +293,7 @@ def func(ctx, **opts): # Build hooks from driver name hooks = {} if driver == 'mongodb': - if not MONGODB_ADDON_ENABLED: + if not ADDONS_ENABLED['mongo']: _get_rich_console().print('[bold red]Missing MongoDB dependencies: please run `secator install addons mongodb`[/].') sys.exit(1) from secator.hooks.mongodb import MONGODB_HOOKS diff --git a/secator/definitions.py b/secator/definitions.py index e0a0186d..ed4053a2 100644 --- a/secator/definitions.py +++ b/secator/definitions.py @@ -4,37 +4,20 @@ import requests from dotenv import find_dotenv, load_dotenv -from pkg_resources import get_distribution, parse_version +from pkg_resources import get_distribution from secator.rich import console load_dotenv(find_dotenv(usecwd=True), override=False) - -def get_latest_version(): - """Get latest secator version from GitHub API.""" - try: - resp = requests.get('https://api.github.com/repos/freelabz/secator/releases/latest', timeout=2) - resp.raise_for_status() - latest_version = resp.json()['name'].lstrip('v') - return latest_version - except requests.exceptions.RequestException as e: - console.print(f'[bold red]Failed to get latest version from GitHub: {type(e).__name__}.') - return None - - # Globals VERSION = get_distribution('secator').version -VERSION_LATEST = get_latest_version() -VERSION_OBSOLETE = parse_version(VERSION_LATEST) > parse_version(VERSION) if VERSION_LATEST else False -VERSION_STR = f'{VERSION} [bold red](outdated)[/]' if VERSION_OBSOLETE else VERSION - ASCII = f""" __ ________ _________ _/ /_____ _____ / ___/ _ \/ ___/ __ `/ __/ __ \/ ___/ (__ / __/ /__/ /_/ / /_/ /_/ / / -/____/\___/\___/\__,_/\__/\____/_/ v{VERSION_STR} +/____/\___/\___/\__,_/\__/\____/_/ v{VERSION} freelabz.com """ # noqa: W605,W291 @@ -175,8 +158,9 @@ def get_latest_version(): for folder in [BIN_FOLDER, DATA_FOLDER, REPORTS_FOLDER, WORDLISTS_FOLDER, SCRIPTS_FOLDER, CVES_FOLDER, PAYLOADS_FOLDER, REVSHELLS_FOLDER, CELERY_DATA_FOLDER, CELERY_RESULTS_FOLDER]: if not os.path.exists(folder): + console.print(f'[bold turquoise4]Creating folder {folder} ...[/] ', end='') os.makedirs(folder) - console.print(f'[bold turquoise4]Created folder[/] {folder}.') + console.print('[bold green]ok.[/]') # Download default wordlists @@ -185,66 +169,68 @@ def get_latest_version(): wordlist_url = globals()[f'DEFAULT_{wordlist}_WORDLIST_URL'] if not os.path.exists(wordlist_path): try: + console.print(f'[bold turquoise4]Downloading default {wordlist} wordlist {wordlist_path} ...[/] ', end='') resp = requests.get(wordlist_url) with open(wordlist_path, 'w') as f: f.write(resp.text) - console.print(f'[bold turquoise4]Downloaded default {wordlist} wordlist[/] {wordlist_path}.') + console.print('[bold green]ok.[/]') except requests.exceptions.RequestException as e: - console.print(f'[bold red]Failed to download default {wordlist} wordlist: {type(e).__name__}.') + console.print(f'[bold green]failed ({type(e).__name__}).[/]') pass +ADDONS_ENABLED = {} # Check worker addon try: import eventlet # noqa: F401 - WORKER_ADDON_ENABLED = 1 + ADDONS_ENABLED['worker'] = True except ModuleNotFoundError: - WORKER_ADDON_ENABLED = 0 + ADDONS_ENABLED['worker'] = False # Check google addon try: import gspread # noqa: F401 - GOOGLE_ADDON_ENABLED = 1 + ADDONS_ENABLED['google'] = True except ModuleNotFoundError: - GOOGLE_ADDON_ENABLED = 0 + ADDONS_ENABLED['google'] = False # Check mongodb addon try: import pymongo # noqa: F401 - MONGODB_ADDON_ENABLED = 1 + ADDONS_ENABLED['mongodb'] = True except ModuleNotFoundError: - MONGODB_ADDON_ENABLED = 0 + ADDONS_ENABLED['mongodb'] = False # Check redis addon try: import redis # noqa: F401 - REDIS_ADDON_ENABLED = 1 + ADDONS_ENABLED['redis'] = True except ModuleNotFoundError: - REDIS_ADDON_ENABLED = 0 + ADDONS_ENABLED['redis'] = False # Check dev addon try: import flake8 # noqa: F401 - DEV_ADDON_ENABLED = 1 + ADDONS_ENABLED['dev'] = True except ModuleNotFoundError: - DEV_ADDON_ENABLED = 0 + ADDONS_ENABLED['dev'] = False # Check build addon try: import hatch # noqa: F401 - BUILD_ADDON_ENABLED = 1 + ADDONS_ENABLED['build'] = True except ModuleNotFoundError: - BUILD_ADDON_ENABLED = 0 + ADDONS_ENABLED['build'] = False # Check trace addon try: import memray # noqa: F401 - TRACE_ADDON_ENABLED = 1 + ADDONS_ENABLED['trace'] = True except ModuleNotFoundError: - TRACE_ADDON_ENABLED = 0 + ADDONS_ENABLED['trace'] = False # Check dev package if os.path.exists(f'{ROOT_FOLDER}/pyproject.toml'): - DEV_PACKAGE = 1 + DEV_PACKAGE = True else: - DEV_PACKAGE = 0 + DEV_PACKAGE = False diff --git a/secator/installer.py b/secator/installer.py index 225e4938..46e3b692 100644 --- a/secator/installer.py +++ b/secator/installer.py @@ -7,6 +7,8 @@ import zipfile import io +from rich.table import Table + from secator.rich import console from secator.runners import Command from secator.definitions import BIN_FOLDER, GITHUB_TOKEN @@ -77,25 +79,14 @@ def install(cls, github_handle): github_handle (str): A GitHub handle {user}/{repo} Returns: - bool: True if install is successful,, False otherwise. + bool: True if install is successful, False otherwise. """ - owner, repo = tuple(github_handle.split('/')) - releases_url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" - - # Query latest release endpoint - headers = {} - if GITHUB_TOKEN: - headers['Authorization'] = f'Bearer {GITHUB_TOKEN}' - response = requests.get(releases_url, headers=headers) - if response.status_code == 403: - console.print('[bold red]Rate-limited by GitHub API. Retry later or set a GITHUB_TOKEN.') - return False - elif response.status_code == 404: - console.print('[dim red]No GitHub releases found.') + _, repo = tuple(github_handle.split('/')) + latest_release = cls.get_latest_release(github_handle) + if not latest_release: return False # Find the right asset to download - latest_release = response.json() os_identifiers, arch_identifiers = cls._get_platform_identifier() download_url = cls._find_matching_asset(latest_release['assets'], os_identifiers, arch_identifiers) if not download_url: @@ -107,6 +98,39 @@ def install(cls, github_handle): cls._download_and_unpack(download_url, BIN_FOLDER, repo) return True + @classmethod + def get_latest_release(cls, github_handle): + """Get latest release from GitHub. + + Args: + github_handle (str): A GitHub handle {user}/{repo}. + + Returns: + dict: Latest release JSON from GitHub releases. + """ + if not github_handle: + return False + owner, repo = tuple(github_handle.split('/')) + url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + headers = {} + if GITHUB_TOKEN: + headers['Authorization'] = f'Bearer {GITHUB_TOKEN}' + try: + response = requests.get(url, headers=headers, timeout=5) + response.raise_for_status() + latest_release = response.json() + return latest_release + except requests.RequestException as e: + console.print(f'Failed to fetch latest release for {github_handle}: {str(e)}') + return None + + @classmethod + def get_latest_version(cls, github_handle): + latest_release = cls.get_latest_release(github_handle) + if not latest_release: + return None + return latest_release['tag_name'].lstrip('v') + @classmethod def _get_platform_identifier(cls): """Generate lists of possible identifiers for the current platform.""" @@ -159,7 +183,7 @@ def _find_matching_asset(cls, assets, os_identifiers, arch_identifiers): def _download_and_unpack(cls, url, destination, repo_name): """Download and unpack a release asset.""" console.print(f'Downloading and unpacking to {destination}...') - response = requests.get(url) + response = requests.get(url, timeout=5) response.raise_for_status() # Create a temporary directory to extract the archive @@ -190,3 +214,122 @@ def _find_binary_in_directory(cls, directory, binary_name): if file == binary_name: return os.path.join(root, file) return None + + +def which(command): + """Run which on a command. + + Args: + command (str): Command to check. + + Returns: + secator.Command: Command instance. + """ + return Command.execute(f'which {command}', quiet=True, print_errors=False) + + +def get_version(version_cmd): + """Run version command and match first version number found. + + Args: + version_cmd (str): Command to get the version. + + Returns: + str: Version string. + """ + from secator.runners import Command + import re + regex = r'[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*' + ret = Command.execute(version_cmd, quiet=True, print_errors=False) + match = re.findall(regex, ret.output) + if not match: + return '' + return match[0] + + +def get_version_info(name, version_flag=None, github_handle=None, version=None): + """Get version info for a command. + + Args: + name (str): Command name. + version_flag (str): Version flag. + github_handle (str): Github handle. + version (str): Existing version. + + Return: + dict: Version info. + """ + from pkg_resources import parse_version + from secator.installer import GithubInstaller + info = { + 'name': name, + 'installed': False, + 'version': version, + 'latest_version': None, + 'location': None, + 'status': '' + } + + # Get binary path + location = which(name).output + info['location'] = location + + # Get current version + if version_flag: + version_cmd = f'{name} {version_flag}' + version = get_version(version_cmd) + info['version'] = version + + # Get latest version + latest_version = GithubInstaller.get_latest_version(github_handle) + info['latest_version'] = latest_version + + if location: + info['installed'] = True + if version and latest_version: + if parse_version(version) < parse_version(latest_version): + info['status'] = 'outdated' + else: + info['status'] = 'latest' + elif not version: + info['status'] = 'current unknown' + elif not latest_version: + info['status'] = 'latest unknown' + else: + info['status'] = 'missing' + + return info + + +def fmt_health_table_row(version_info, category=None): + name = version_info['name'] + version = version_info['version'] + status = version_info['status'] + installed = version_info['installed'] + name_str = f'[magenta]{name}[/]' + + # Format version row + _version = version or '' + _version = f'[bold green]{_version:<10}[/]' + if status == 'latest': + _version += ' [bold green](latest)[/]' + elif status == 'outdated': + _version += ' [bold red](outdated)[/]' + elif status == 'missing': + _version = '[bold red]missing[/]' + elif status == 'ok': + _version = '[bold green]ok [/]' + elif status: + if not version and installed: + _version = '[bold green]ok [/]' + _version += f' [dim]({status}[/])' + + row = (name_str, _version) + return row + + +def get_health_table(): + table = Table(box=None, show_header=False) + for col in ['name', 'version']: + table.add_column(col) + return table diff --git a/secator/runners/_base.py b/secator/runners/_base.py index 89f19186..2613dc35 100644 --- a/secator/runners/_base.py +++ b/secator/runners/_base.py @@ -106,7 +106,7 @@ def __init__(self, config, targets, results=[], run_opts={}, hooks={}, context={ self.context = context self.delay = run_opts.get('delay', False) self.uuids = [] - self.result = None + self.celery_result = None # Determine report folder default_reports_folder_base = f'{REPORTS_FOLDER}/{self.workspace_name}/{self.config.type}s' @@ -280,9 +280,9 @@ def __iter__(self): except KeyboardInterrupt: self._print('Process was killed manually (CTRL+C / CTRL+X).', color='bold red', rich=True) - if self.result: + if self.celery_result: self._print('Revoking remote Celery tasks ...', color='bold red', rich=True) - self.stop_live_tasks(self.result) + self.stop_live_tasks(self.celery_result) # Filter results and log info self.mark_duplicates() @@ -291,9 +291,10 @@ def __iter__(self): self.run_hooks('on_end') def mark_duplicates(self): - debug('duplicate check', id=self.config.name, sub='runner.mark_duplicates') + debug('running duplicate check', id=self.config.name, sub='runner.mark_duplicates') + dupe_count = 0 for item in self.results: - debug('duplicate check', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=2) + debug('running duplicate check', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=5) others = [f for f in self.results if f == item and f._uuid != item._uuid] if others: main = max(item, *others) @@ -313,13 +314,16 @@ def mark_duplicates(self): if not dupe._duplicate: debug( 'found new duplicate', obj=dupe.toDict(), obj_breaklines=True, - sub='runner.mark_duplicates', level=2) + sub='runner.mark_duplicates', level=5) + dupe_count += 1 dupe._duplicate = True dupe = self.run_hooks('on_duplicate', dupe) - debug('Duplicates:', sub='runner.mark_duplicates', level=2) - debug('\n\t'.join([repr(i) for i in self.results if i._duplicate]), sub='runner.mark_duplicates', level=2) - debug('duplicate check completed', id=self.config.name, sub='runner.mark_duplicates') + duplicates = [repr(i) for i in self.results if i._duplicate] + if duplicates: + duplicates_str = '\n\t'.join(duplicates) + debug(f'Duplicates ({dupe_count}):\n\t{duplicates_str}', sub='runner.mark_duplicates', level=5) + debug(f'duplicate check completed: {dupe_count} found', id=self.config.name, sub='runner.mark_duplicates') def yielder(self): raise NotImplementedError() diff --git a/secator/runners/task.py b/secator/runners/task.py index 3fe0f096..8819ce51 100644 --- a/secator/runners/task.py +++ b/secator/runners/task.py @@ -39,7 +39,8 @@ def yielder(self): 'print_input_file': DEBUG > 0, 'print_item': True, 'print_item_count': not self.sync and not dry_run, - 'print_line': self.sync and not self.output_quiet, + 'print_line': True + # 'print_line': self.sync and not self.output_quiet, } # self.print_item = not self.sync # enable print_item for base Task only if running remote run_opts.update(fmt_opts) @@ -59,9 +60,9 @@ def yielder(self): if dry_run: # don't run return else: - result = task_cls.delay(self.targets, **run_opts) + self.celery_result = task_cls.delay(self.targets, **run_opts) task = self.process_live_tasks( - result, + self.celery_result, description=False, results_only=True, print_remote_status=self.print_remote_status) diff --git a/secator/runners/workflow.py b/secator/runners/workflow.py index 33f6f9d7..e04e8c21 100644 --- a/secator/runners/workflow.py +++ b/secator/runners/workflow.py @@ -58,7 +58,7 @@ def yielder(self): results = workflow.apply().get() else: result = workflow() - self.result = result + self.celery_result = result results = self.process_live_tasks(result, results_only=True, print_remote_status=self.print_remote_status) # Get workflow results diff --git a/secator/tasks/searchsploit.py b/secator/tasks/searchsploit.py index e8b21406..3cacd597 100644 --- a/secator/tasks/searchsploit.py +++ b/secator/tasks/searchsploit.py @@ -28,6 +28,7 @@ class searchsploit(Command): } } install_cmd = 'sudo git clone https://gitlab.com/exploit-database/exploitdb.git /opt/exploitdb || true && sudo ln -sf /opt/exploitdb/searchsploit /usr/local/bin/searchsploit' # noqa: E501 + install_github_handle = 'rad10/SearchSploit.py' proxychains = False proxy_socks5 = False proxy_http = False diff --git a/secator/utils.py b/secator/utils.py index 14f08a82..2714602c 100644 --- a/secator/utils.py +++ b/secator/utils.py @@ -15,11 +15,13 @@ from pkgutil import iter_modules from urllib.parse import urlparse, quote + import ifaddr import yaml from rich.markdown import Markdown -from secator.definitions import DEBUG, DEBUG_COMPONENT, DEFAULT_STDIN_TIMEOUT +from secator.definitions import (DEBUG, DEBUG_COMPONENT, DEFAULT_STDIN_TIMEOUT, VERSION, DEV_PACKAGE, ROOT_FOLDER, + LIB_FOLDER) from secator.rich import console logger = logging.getLogger(__name__) @@ -402,3 +404,23 @@ def escape_mongodb_url(url): user, password = quote(user), quote(password) return f'mongodb://{user}:{password}@{url}' return url + + +def print_version(): + """Print secator version information.""" + from secator.installer import get_version_info + console.print(f'[bold gold3]Current version[/]: {VERSION}', highlight=False, end='') + info = get_version_info('secator', github_handle='freelabz/secator', version=VERSION) + latest_version = info['latest_version'] + status = info['status'] + location = info['location'] + if status == 'outdated': + console.print('[bold red] (outdated)[/]') + console.print(f'[bold gold3]Latest version[/]: {latest_version}', highlight=False) + console.print(f'[bold gold3]Location[/]: {location}') + console.print(f'[bold gold3]Python binary[/]: {sys.executable}') + if DEV_PACKAGE: + console.print(f'[bold gold3]Root folder[/]: {ROOT_FOLDER}') + console.print(f'[bold gold3]Lib folder[/]: {LIB_FOLDER}') + if status == 'outdated': + console.print('[bold red]secator is outdated, run "secator update" to install the latest version.')