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 |
@@ -83,27 +83,7 @@
-
-
- {% for path, uid_tuple_list in result['plugins'][plugin][feature].items() | sort %}
- {% for (uid_1, uid_2) in uid_tuple_list %}
-
- |
- {{ path }}
- |
-
- {{ path }}
- |
-
-
- |
-
- {% endfor %}
- {% endfor %}
-
-
+ {{ 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(
+ [
+ '',
+ ' ',
+ f' ',
+ f' {key}',
+ ' | ',
+ ' ',
+ f' ',
+ f' {key}',
+ ' | ',
+ ' ',
+ f' ',
+ ' | ',
+ ' ',
+ ]
+ )
+ lines.append(' ')
+ 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
|