Skip to content

Commit

Permalink
feat(cli): add report list / export commands (#367)
Browse files Browse the repository at this point in the history
  • Loading branch information
ocervell committed May 2, 2024
1 parent b5d7874 commit ab396a3
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 21 deletions.
69 changes: 61 additions & 8 deletions secator/cli.py
Expand Up @@ -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

Expand Down Expand Up @@ -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()


#--------#
Expand Down
5 changes: 1 addition & 4 deletions 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


Expand All @@ -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:
Expand Down
21 changes: 12 additions & 9 deletions secator/runners/_base.py
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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."""
Expand Down

0 comments on commit ab396a3

Please sign in to comment.