diff --git a/secator/cli.py b/secator/cli.py index 2939c9a8..1be9988a 100644 --- a/secator/cli.py +++ b/secator/cli.py @@ -20,7 +20,8 @@ from secator.definitions import ADDONS_ENABLED, ASCII, DEV_PACKAGE, OPT_NOT_SUPPORTED, VERSION from secator.installer import ToolInstaller, fmt_health_table_row, get_health_table, get_version_info from secator.rich import console -from secator.runners import Command +from secator.runners import Command, Runner +from secator.report import Report from secator.serializers.dataclass import loads_dataclass from secator.utils import debug, detect_host, discover_tasks, flatten, print_results_table, print_version @@ -536,17 +537,69 @@ def report(): @report.command('show') @click.argument('json_path') +@click.option('-o', '--output', type=str, default='console', help='Format') @click.option('-e', '--exclude-fields', type=str, default='', help='List of fields to exclude (comma-separated)') -def report_show(json_path, exclude_fields): - """Show a JSON report as a nicely-formatted table.""" +def report_show(json_path, output, exclude_fields): + """Show a JSON report.""" with open(json_path, 'r') as f: report = loads_dataclass(f.read()) results = flatten(list(report['results'].values())) - exclude_fields = exclude_fields.split(',') - print_results_table( - results, - title=report['info']['title'], - exclude_fields=exclude_fields) + if output == 'console': + for result in results: + console.print(result) + elif output == 'table': + exclude_fields = exclude_fields.split(',') + print_results_table( + results, + title=report['info']['title'], + exclude_fields=exclude_fields) + + +@report.command('list') +@click.option('-ws', '--workspace', type=str) +def report_list(workspace): + reports_dir = CONFIG.dirs.reports + json_reports = reports_dir.glob("**/**/report.json") + ws_reports = {} + for report in json_reports: + ws, runner, number = str(report).split('/')[-4:-1] + if ws not in ws_reports: + ws_reports[ws] = [] + with open(report, 'r') as f: + content = json.loads(f.read()) + data = {'path': report, 'name': content['info']['name'], 'runner': runner} + ws_reports[ws].append(data) + + for ws in ws_reports: + if workspace and not ws == workspace: + continue + console.print(f'[bold gold3]{ws}:') + for data in sorted(ws_reports[ws], key=lambda x: x['path']): + console.print(f' • {data["path"]} ([bold blue]{data["name"]}[/] [dim]{data["runner"][:-1]}[/])') + + +@report.command('export') +@click.argument('json_path', type=str) +@click.option('--output-folder', '-of', type=str) +@click.option('-output', '-o', type=str) +def report_export(json_path, output_folder, output): + with open(json_path, 'r') as f: + data = loads_dataclass(f.read()) + flatten(list(data['results'].values())) + + runner_instance = DotMap({ + "config": { + "name": data['info']['name'] + }, + "workspace_name": json_path.split('/')[-4], + "reports_folder": output_folder or Path.cwd(), + "data": data, + "results": flatten(list(data['results'].values())) + }) + exporters = Runner.resolve_exporters(output) + report = Report(runner_instance, title=data['info']['title'], exporters=exporters) + report.data = data + report.send() #--------# diff --git a/secator/report.py b/secator/report.py index dcc45cfc..69b3b2da 100644 --- a/secator/report.py +++ b/secator/report.py @@ -1,7 +1,7 @@ import operator from secator.output_types import OUTPUT_TYPES, OutputType -from secator.utils import merge_opts, get_file_timestamp, print_results_table +from secator.utils import merge_opts, get_file_timestamp from secator.rich import console @@ -22,9 +22,6 @@ def __init__(self, runner, title=None, exporters=[]): self.workspace_name = runner.workspace_name self.output_folder = runner.reports_folder - def as_table(self): - print_results_table(self.results, self.title) - def send(self): for report_cls in self.exporters: try: diff --git a/secator/runners/_base.py b/secator/runners/_base.py index 7adb6767..2117d603 100644 --- a/secator/runners/_base.py +++ b/secator/runners/_base.py @@ -92,7 +92,6 @@ def __init__(self, config, targets, results=[], run_opts={}, hooks={}, context={ self.workspace_name = context.get('workspace_name', 'default') self.run_opts = run_opts.copy() self.sync = run_opts.get('sync', True) - self.exporters = self.resolve_exporters() self.done = False self.start_time = datetime.fromtimestamp(time()) self.last_updated = None @@ -109,6 +108,10 @@ def __init__(self, config, targets, results=[], run_opts={}, hooks={}, context={ self.uuids = [] self.celery_result = None + # Determine exporters + exporters_str = self.run_opts.get('output') or self.default_exporters + self.exporters = Runner.resolve_exporters(exporters_str) + # Determine report folder default_reports_folder_base = f'{CONFIG.dirs.reports}/{self.workspace_name}/{self.config.type}s' _id = get_task_folder_id(default_reports_folder_base) @@ -390,19 +393,19 @@ def run_validators(self, validator_type, *args): return False return True - def resolve_exporters(self): + @staticmethod + def resolve_exporters(exporters): """Resolve exporters from output options.""" - output = self.run_opts.get('output') or self.default_exporters - if not output or output in ['false', 'False']: + if not exporters or exporters in ['false', 'False']: return [] - if isinstance(output, str): - output = output.split(',') - exporters = [ + if isinstance(exporters, str): + exporters = exporters.split(',') + classes = [ import_dynamic(f'secator.exporters.{o.capitalize()}Exporter', 'Exporter') - for o in output + for o in exporters if o ] - return [e for e in exporters if e] + return [cls for cls in classes if cls] def log_start(self): """Log runner start."""