diff --git a/src/plugins/compare/file_coverage/view/file_coverage.html b/src/plugins/compare/file_coverage/view/file_coverage.html index 0f823247d..4fa1781eb 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 6b63640b9..a743eceba 100644 --- a/src/test/unit/web_interface/test_filter.py +++ b/src/test/unit/web_interface/test_filter.py @@ -314,6 +314,18 @@ def test_group_dict_list_by_key(list_of_dicts, key, expected_result): assert flt.group_dict_list_by_key(list_of_dicts, key) == 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 104601e80..25b535a4b 100644 --- a/src/web_interface/components/jinja_filter.py +++ b/src/web_interface/components/jinja_filter.py @@ -180,6 +180,7 @@ def _setup_filters(self): 'get_searchable_crypto_block': flt.get_searchable_crypto_block, 'get_unique_keys_from_list_of_dicts': flt.get_unique_keys_from_list_of_dicts, 'group_dict_list_by_key': flt.group_dict_list_by_key, + 'group_changed_text_files': flt.group_path_dict_by_dirs, 'hex': hex, 'hide_dts_binary_data': flt.hide_dts_binary_data, 'infection_color': flt.infection_color, @@ -207,6 +208,7 @@ def _setup_filters(self): 'regex_meta': flt.comment_out_regex_meta_chars, 'remaining_time': elapsed_time, 'render_analysis_tags': flt.render_analysis_tags, + 'render_changed_text_files': flt.render_changed_text_files, 'render_general_information': self._render_general_information_table, 'render_query_title': flt.render_query_title, 'render_fw_tags': flt.render_fw_tags, diff --git a/src/web_interface/filter.py b/src/web_interface/filter.py index 01a885d7f..47f0e40bc 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 @@ -25,6 +26,7 @@ from helperFunctions.data_conversion import make_unicode_string from helperFunctions.tag import TagColor from helperFunctions.web_interface import get_alternating_color_list +from web_interface.file_tree.file_tree import get_icon_for_mime, get_mime_for_text_file from web_interface.security.authentication import user_has_privilege from web_interface.security.privileges import PRIVILEGES @@ -376,6 +378,20 @@ def group_dict_list_by_key(dict_list: list[dict], key: Any) -> dict[str, list[di return result +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)) @@ -397,6 +413,66 @@ 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()): + icon = get_icon_for_mime(get_mime_for_text_file(key)) + 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' ', + f' {key}', + ' ', + f' ', + 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