From af820024fbd968fa8548c96d0d2a00b56fc6dcb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Mon, 7 Oct 2024 16:38:00 +0200 Subject: [PATCH] feat: tree structure for comparison changed text files --- .../file_coverage/view/file_coverage.html | 24 +----- src/test/unit/web_interface/test_filter.py | 12 +++ src/web_interface/components/jinja_filter.py | 2 + src/web_interface/filter.py | 74 +++++++++++++++++++ 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/src/plugins/compare/file_coverage/view/file_coverage.html b/src/plugins/compare/file_coverage/view/file_coverage.html index 0f823247d8..4fa1781eb5 100644 --- a/src/plugins/compare/file_coverage/view/file_coverage.html +++ b/src/plugins/compare/file_coverage/view/file_coverage.html @@ -75,7 +75,7 @@ {% elif feature == 'changed_text_files' %} - diverging text files + changed text files
-
- - {% for path, uid_tuple_list in result['plugins'][plugin][feature].items() | sort %} - {% for (uid_1, uid_2) in uid_tuple_list %} - - - - - - {% endfor %} - {% endfor %} -
- {{ path }} - - {{ path }} - - -
-
+ {{ result['plugins'][plugin][feature] | group_changed_text_files | render_changed_text_files | safe }}
diff --git a/src/test/unit/web_interface/test_filter.py b/src/test/unit/web_interface/test_filter.py index 5aa55dfbc3..6afb3182b2 100644 --- a/src/test/unit/web_interface/test_filter.py +++ b/src/test/unit/web_interface/test_filter.py @@ -302,6 +302,18 @@ def test_get_unique_keys_from_list_of_dicts(list_of_dicts, expected_result): assert flt.get_unique_keys_from_list_of_dicts(list_of_dicts) == expected_result +@pytest.mark.parametrize( + ('input_dict', 'expected_result'), + [ + ({}, {}), + ({'a': 1}, {'a': 1}), + ({'/a/b/c': 1, '/a/b/d': 2, '/a/e': 3}, {'a': {'b': {'c': 1, 'd': 2}, 'e': 3}}), + ], +) +def test_grp_changed_text_file_data(input_dict, expected_result): + assert flt.group_path_dict_by_dirs(input_dict) == expected_result + + @pytest.mark.parametrize( ('function', 'input_data', 'expected_output', 'error_message'), [ diff --git a/src/web_interface/components/jinja_filter.py b/src/web_interface/components/jinja_filter.py index 71bd04bcca..0128ffab24 100644 --- a/src/web_interface/components/jinja_filter.py +++ b/src/web_interface/components/jinja_filter.py @@ -195,6 +195,7 @@ def _setup_filters(self): # noqa: PLR0915 self._app.jinja_env.filters['get_canvas_height'] = flt.get_canvas_height self._app.jinja_env.filters['get_searchable_crypto_block'] = flt.get_searchable_crypto_block self._app.jinja_env.filters['get_unique_keys_from_list_of_dicts'] = flt.get_unique_keys_from_list_of_dicts + self._app.jinja_env.filters['group_changed_text_files'] = flt.group_path_dict_by_dirs self._app.jinja_env.filters['hex'] = hex self._app.jinja_env.filters['hide_dts_binary_data'] = flt.hide_dts_binary_data self._app.jinja_env.filters['infection_color'] = flt.infection_color @@ -221,6 +222,7 @@ def _setup_filters(self): # noqa: PLR0915 self._app.jinja_env.filters['remaining_time'] = elapsed_time self._app.jinja_env.filters['render_analysis_tags'] = flt.render_analysis_tags self._app.jinja_env.filters['render_general_information'] = self._render_general_information_table + self._app.jinja_env.filters['render_changed_text_files'] = flt.render_changed_text_files self._app.jinja_env.filters['render_query_title'] = flt.render_query_title self._app.jinja_env.filters['render_fw_tags'] = flt.render_fw_tags self._app.jinja_env.filters['replace_comparison_uid_with_hid'] = self._filter_replace_comparison_uid_with_hid diff --git a/src/web_interface/filter.py b/src/web_interface/filter.py index b41eb744af..d52ccf942f 100644 --- a/src/web_interface/filter.py +++ b/src/web_interface/filter.py @@ -3,6 +3,7 @@ import binascii import json import logging +import os import random import re import stat @@ -366,6 +367,20 @@ def get_unique_keys_from_list_of_dicts(list_of_dicts: list[dict]): return unique_keys +def group_path_dict_by_dirs(data: dict[str, list[tuple]]) -> dict: + # groups paths into folders, resulting in a tree structure + result = {} + for path, value in data.items(): + current = result + # path is expected to be of structure /dir1/dir2/.../file + *folders, file = path.split('/') + for dir_name in folders: + if dir_name: + current = current.setdefault(dir_name, {}) + current[file] = value + return result + + def random_collapse_id(): return ''.join(random.choice(ascii_letters) for _ in range(10)) @@ -387,6 +402,65 @@ def format_duration(duration: float) -> str: return str(timedelta(seconds=duration)) +def render_changed_text_files(changed_text_files: dict) -> str: + elements = [] + for key, value in sorted(changed_text_files.items()): + if isinstance(value, list): + # file tuple list (represents a leaf/file in the file tree) + lines = [''] + for uid_1, uid_2 in value: + lines.extend( + [ + '', + ' ', + ' ', + ' ', + '', + ] + ) + lines.append('
', + f' {key}', + ' ', + f' {key}', + ' ', + f' ', + '
') + element = '\n'.join(lines) + else: + # directory dict (represents an inner node/folder in the file tree) + inner = render_changed_text_files(value) + id_ = f'ctf-{os.urandom(8).hex()}' + count = _count_changed_files(value) + element = ( + f'
' + f' ╰ {key} ' + f' {count}' + f'
\n' + f'
\n' + f' {inner}\n' + '
\n' + ) + elements.append( + f'
\n' + f'{element}\n' + '
' + ) + return '\n'.join(elements) + + +def _count_changed_files(ctf_dict: dict) -> int: + count = 0 + for value in ctf_dict.values(): + if isinstance(value, dict): + count += _count_changed_files(value) + elif isinstance(value, list): + count += len(value) + return count + + def render_query_title(query_title: None | str | dict): if query_title is None: return None