From f2b9250dc5dd37e955192c044a8e3c6007995c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Stucke?= Date: Mon, 29 Jul 2024 17:08:57 +0200 Subject: [PATCH] plugin users&pws: ported to new base class --- .../code/password_file_analyzer.py | 195 +++++------------- .../users_and_passwords/internal/__init__.py | 0 .../internal/crack_password.py | 43 ++++ .../internal/credentials_finder.py | 121 +++++++++++ .../test_plugin_password_file_analyzer.py | 123 +++++------ .../view/users_and_passwords.html | 64 +++--- .../test_app_jinja_filter_static.py | 9 - src/web_interface/components/jinja_filter.py | 14 -- 8 files changed, 311 insertions(+), 258 deletions(-) create mode 100644 src/plugins/analysis/users_and_passwords/internal/__init__.py create mode 100644 src/plugins/analysis/users_and_passwords/internal/crack_password.py create mode 100644 src/plugins/analysis/users_and_passwords/internal/credentials_finder.py diff --git a/src/plugins/analysis/users_and_passwords/code/password_file_analyzer.py b/src/plugins/analysis/users_and_passwords/code/password_file_analyzer.py index 524490e4a..77e325ed5 100644 --- a/src/plugins/analysis/users_and_passwords/code/password_file_analyzer.py +++ b/src/plugins/analysis/users_and_passwords/code/password_file_analyzer.py @@ -1,152 +1,67 @@ from __future__ import annotations -import logging -import re -from base64 import b64decode -from contextlib import suppress +from itertools import chain from pathlib import Path -from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List -from docker.types import Mount +import pydantic +from pydantic import Field -from analysis.PluginBase import AnalysisBasePlugin -from helperFunctions.docker import run_docker_container -from helperFunctions.fileSystem import get_src_dir +from analysis.plugin import AnalysisPluginV0, Tag +from analysis.plugin.compat import AnalysisBasePluginAdapterMixin from helperFunctions.tag import TagColor +from plugins.analysis.users_and_passwords.internal.credentials_finder import ( + CredentialResult, + HtpasswdCredentialFinder, + MosquittoCredentialFinder, + UnixCredentialFinder, +) from plugins.mime_blacklists import MIME_BLACKLIST_NON_EXECUTABLE if TYPE_CHECKING: - from collections.abc import Callable - - from objects.file import FileObject - -JOHN_PATH = Path(__file__).parent.parent / 'bin' / 'john' -JOHN_POT = Path(__file__).parent.parent / 'bin' / 'john.pot' -WORDLIST_PATH = Path(get_src_dir()) / 'bin' / 'passwords.txt' -USER_NAME_REGEX = rb'[a-zA-Z][a-zA-Z0-9_-]{2,15}' -UNIX_REGEXES = [ - USER_NAME_REGEX + rb':[^:]?:\d+:\d*:[^:]*:[^:]*:[^\n ]*', - USER_NAME_REGEX + rb':\$[1256][ay]?\$[a-zA-Z0-9\./+]*\$[a-zA-Z0-9\./+]{16,128}={0,2}', # MD5 / Blowfish / SHA - USER_NAME_REGEX + rb':[a-zA-Z0-9\./=]{13}:\d*:\d*:', # DES -] -HTPASSWD_REGEXES = [ - USER_NAME_REGEX + rb':\$apr1\$[a-zA-Z0-9\./+=]+\$[a-zA-Z0-9\./+]{22}', # MD5 apr1 - USER_NAME_REGEX + rb':\{SHA\}[a-zA-Z0-9\./+]{27}=', # SHA-1 -] -MOSQUITTO_REGEXES = [rb'[a-zA-Z][a-zA-Z0-9_-]{2,15}\:\$6\$[a-zA-Z0-9+/=]+\$[a-zA-Z0-9+/]{86}=='] -RESULTS_DELIMITER = '=== Results: ===' - - -class AnalysisPlugin(AnalysisBasePlugin): - """ - This plug-in tries to find and crack passwords - """ - - NAME = 'users_and_passwords' - DEPENDENCIES = [] # noqa: RUF012 - MIME_BLACKLIST = MIME_BLACKLIST_NON_EXECUTABLE - DESCRIPTION = 'search for UNIX, httpd, and mosquitto password files, parse them and try to crack the passwords' - VERSION = '0.5.4' - FILE = __file__ - - def process_object(self, file_object: FileObject) -> FileObject: - if self.NAME not in file_object.processed_analysis: - file_object.processed_analysis[self.NAME] = {} - file_object.processed_analysis[self.NAME]['summary'] = [] - self.find_password_entries(file_object, UNIX_REGEXES, generate_unix_entry) - self.find_password_entries(file_object, HTPASSWD_REGEXES, generate_htpasswd_entry) - self.find_password_entries(file_object, MOSQUITTO_REGEXES, generate_mosquitto_entry) - return file_object - - def find_password_entries(self, file_object: FileObject, regex_list: list[bytes], entry_gen_function: Callable): - for passwd_regex in regex_list: - passwd_entries = re.findall(passwd_regex, file_object.binary) - for entry in passwd_entries: - self.update_file_object(file_object, entry_gen_function(entry)) - - def _add_found_password_tag(self, file_object: FileObject, result: dict): - for password_entry in result: - if 'password' in result[password_entry]: - username = password_entry.split(':', 1)[0] - password = result[password_entry]['password'] - self.add_analysis_tag( - file_object, f'{username}_{password}', f'Password: {username}:{password}', TagColor.RED, True - ) - - def update_file_object(self, file_object: FileObject, result_entry: dict): - file_object.processed_analysis[self.NAME].update(result_entry) - file_object.processed_analysis[self.NAME]['summary'].extend(list(result_entry)) - self._add_found_password_tag(file_object, result_entry) - - -def generate_unix_entry(entry: bytes) -> dict: - user_name, pw_hash, *_ = entry.split(b':') - result_entry = {'type': 'unix', 'entry': _to_str(entry)} - try: - if pw_hash.startswith(b'$') or _is_des_hash(pw_hash): - result_entry['password-hash'] = _to_str(pw_hash) - result_entry['cracked'] = crack_hash(b':'.join((user_name, pw_hash)), result_entry) - except (IndexError, AttributeError, TypeError): - logging.warning(f'Unsupported password format: {entry}', exc_info=True) - return {f'{_to_str(user_name)}:unix': result_entry} - - -def generate_htpasswd_entry(entry: bytes) -> dict: - user_name, pw_hash = entry.split(b':') - result_entry = {'type': 'htpasswd', 'entry': _to_str(entry), 'password-hash': _to_str(pw_hash)} - result_entry['cracked'] = crack_hash(entry, result_entry) - return {f'{_to_str(user_name)}:htpasswd': result_entry} - - -def generate_mosquitto_entry(entry: bytes) -> dict: - entry_decoded = _to_str(entry) - user, _, _, salt_hash, passwd_hash, *_ = re.split(r'[:$]', entry_decoded) - passwd_entry = f'{user}:$dynamic_82${b64decode(passwd_hash).hex()}$HEX${b64decode(salt_hash).hex()}' - result_entry = {'type': 'mosquitto', 'entry': entry_decoded, 'password-hash': passwd_hash} - result_entry['cracked'] = crack_hash(passwd_entry.encode(), result_entry, '--format=dynamic_82') - return {f'{user}:mosquitto': result_entry} - - -def _is_des_hash(pw_hash: str) -> bool: - return len(pw_hash) == 13 # noqa: PLR2004 - - -def crack_hash(passwd_entry: bytes, result_entry: dict, format_term: str = '') -> bool: - with NamedTemporaryFile() as fp: - fp.write(passwd_entry) - fp.seek(0) - john_process = run_docker_container( - 'fact/john:alpine-3.18', - command=f'/work/input_file {format_term}', - mounts=[ - Mount('/work/input_file', fp.name, type='bind'), - Mount('/root/.john/john.pot', str(JOHN_POT), type='bind'), - ], - logging_label='users_and_passwords', + from io import FileIO + + +class AnalysisPlugin(AnalysisPluginV0, AnalysisBasePluginAdapterMixin): + class Schema(pydantic.BaseModel): + unix: List[CredentialResult] = Field(description='The list of found UNIX credentials.') + http: List[CredentialResult] = Field(description='The list of found HTTP basic auth credentials.') + mosquitto: List[CredentialResult] = Field(description='The list of found Mosquitto MQTT broker credentials.') + + def __init__(self): + super().__init__( + metadata=AnalysisPluginV0.MetaData( + name='users_and_passwords', + description=( + 'search for UNIX, httpd, and mosquitto password files, parse them and try to crack the passwords' + ), + version='1.0.0', + Schema=self.Schema, + mime_blacklist=MIME_BLACKLIST_NON_EXECUTABLE, + ), ) - result_entry['log'] = john_process.stdout - if 'No password hashes loaded' in john_process.stdout: - result_entry['ERROR'] = 'hash type is not supported' - return False - output = parse_john_output(john_process.stdout) - if output: - if any('0 password hashes cracked' in line for line in output): - result_entry['ERROR'] = 'password cracking not successful' - return False - with suppress(IndexError): - result_entry['password'] = output[0].split(':')[1] - return True - return False - - -def parse_john_output(john_output: str) -> list[str]: - if RESULTS_DELIMITER in john_output: - start_offset = john_output.find(RESULTS_DELIMITER) + len(RESULTS_DELIMITER) + 1 # +1 is '\n' after delimiter - return [line for line in john_output[start_offset:].split('\n') if line] - return [] + def analyze(self, file_handle: FileIO, virtual_file_path: dict[str, list[str]], analyses: dict) -> Schema: + del virtual_file_path, analyses + file_contents = Path(file_handle.name).read_bytes() + return self.Schema( + unix=UnixCredentialFinder.find_credentials(file_contents), + http=HtpasswdCredentialFinder.find_credentials(file_contents), + mosquitto=MosquittoCredentialFinder.find_credentials(file_contents), + ) -def _to_str(byte_str: bytes) -> str: - """result entries must be converted from `bytes` to `str` in order to be saved as JSON""" - return byte_str.decode(errors='replace') + def summarize(self, result: Schema) -> list[str]: + return [f'{entry.username}:{entry.type}' for entry in chain(result.unix, result.http, result.mosquitto)] + + def get_tags(self, result: Schema, summary: list[str]) -> list[Tag]: + del summary + return [ + Tag( + name=f'{entry.username}_{entry.password}', + value=f'Password: {entry.username}:{entry.password}', + color=TagColor.RED, + propagate=True, + ) + for entry in chain(result.unix, result.http, result.mosquitto) + if entry.password + ] diff --git a/src/plugins/analysis/users_and_passwords/internal/__init__.py b/src/plugins/analysis/users_and_passwords/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/plugins/analysis/users_and_passwords/internal/crack_password.py b/src/plugins/analysis/users_and_passwords/internal/crack_password.py new file mode 100644 index 000000000..2297c04a2 --- /dev/null +++ b/src/plugins/analysis/users_and_passwords/internal/crack_password.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from contextlib import suppress +from pathlib import Path +from tempfile import NamedTemporaryFile + +from docker.types import Mount + +from helperFunctions.docker import run_docker_container + +JOHN_POT = Path(__file__).parent.parent / 'bin' / 'john.pot' +RESULTS_DELIMITER = '=== Results: ===' + + +def crack_hash(passwd_entry: bytes, format_term: str = '') -> tuple[str | None, str | None]: + with NamedTemporaryFile() as fp: + fp.write(passwd_entry) + fp.seek(0) + john_process = run_docker_container( + 'fact/john:alpine-3.18', + command=f'/work/input_file {format_term}', + mounts=[ + Mount('/work/input_file', fp.name, type='bind'), + Mount('/root/.john/john.pot', str(JOHN_POT), type='bind'), + ], + logging_label='users_and_passwords', + ) + if 'No password hashes loaded' in john_process.stdout: + return None, 'hash type is not supported' + output = _parse_john_output(john_process.stdout) + if output: + if any('0 password hashes cracked' in line for line in output): + return None, 'password cracking not successful' + with suppress(IndexError): + return output[0].split(':')[1], None + return None, None + + +def _parse_john_output(john_output: str) -> list[str]: + if RESULTS_DELIMITER in john_output: + start_offset = john_output.find(RESULTS_DELIMITER) + len(RESULTS_DELIMITER) + 1 # +1 is '\n' after delimiter + return [line for line in john_output[start_offset:].split('\n') if line] + return [] diff --git a/src/plugins/analysis/users_and_passwords/internal/credentials_finder.py b/src/plugins/analysis/users_and_passwords/internal/credentials_finder.py new file mode 100644 index 000000000..9a23ba6b7 --- /dev/null +++ b/src/plugins/analysis/users_and_passwords/internal/credentials_finder.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import abc +import logging +import re +from base64 import b64decode +from typing import Optional + +import pydantic +from pydantic import Field + +from .crack_password import crack_hash + +USER_NAME_REGEX = rb'[a-zA-Z][a-zA-Z0-9_-]{2,15}' +DES_HASH_LENGTH = 13 + + +class CredentialResult(pydantic.BaseModel): + username: str = Field(description='The username.') + full_entry: str = Field(description='The full entry in unparsed form.') + type: str = Field(description='The type of credential (UNIX, htpasswd, etc.).') + password_hash: Optional[str] = Field(description='The password in hashed form.', default=None) + password: Optional[str] = Field(description='The password (if the hash was available and cracked).', default=None) + error: Optional[str] = Field( + description='Error message (if cracking the password hash was not successful).', + default=None, + ) + + +class CredentialFinder(abc.ABC): + REGEX_LIST: tuple[re.Pattern] + + @classmethod + def find_credentials(cls, file_contents: bytes) -> list[CredentialResult]: + return [ + cls._parse_entry(pw_entry) + for passwd_regex in cls.REGEX_LIST + for pw_entry in passwd_regex.findall(file_contents) + ] + + @staticmethod + @abc.abstractmethod + def _parse_entry(entry: bytes) -> CredentialResult: + ... + + +class UnixCredentialFinder(CredentialFinder): + REGEX_LIST = ( + re.compile(USER_NAME_REGEX + rb':[^:]?:\d+:\d*:[^:]*:[^:]*:[^\n ]*'), + # MD5 / Blowfish / SHA + re.compile(USER_NAME_REGEX + rb':\$[1256][ay]?\$[a-zA-Z0-9./+]*\$[a-zA-Z0-9./+]{16,128}={0,2}'), + re.compile(USER_NAME_REGEX + rb':[a-zA-Z0-9./=]{13}:\d*:\d*:'), # DES + ) + + @staticmethod + def _parse_entry(entry: bytes) -> CredentialResult: + user_name, pw_hash, *_ = entry.split(b':') + password, error = None, None + try: + if pw_hash.startswith(b'$') or _is_des_hash(pw_hash): + password, error = crack_hash(b':'.join((user_name, pw_hash))) + except (IndexError, AttributeError, TypeError): + error = f'Unsupported password format: {entry}' + logging.warning(error, exc_info=True) + return CredentialResult( + username=_to_str(user_name), + full_entry=_to_str(entry), + type='unix', + password_hash=_to_str(pw_hash), + password=password, + error=error, + ) + + +def _is_des_hash(pw_hash: str) -> bool: + return len(pw_hash) == DES_HASH_LENGTH + + +class HtpasswdCredentialFinder(CredentialFinder): + REGEX_LIST = ( + re.compile(USER_NAME_REGEX + rb':\$apr1\$[a-zA-Z0-9./+=]+\$[a-zA-Z0-9./+]{22}'), # MD5 apr1 + re.compile(USER_NAME_REGEX + rb':\{SHA}[a-zA-Z0-9./+]{27}='), # SHA-1 + ) + + @staticmethod + def _parse_entry(entry: bytes) -> CredentialResult: + user_name, pw_hash = entry.split(b':') + password, error = crack_hash(entry) + return CredentialResult( + username=_to_str(user_name), + full_entry=_to_str(entry), + type='http', + password_hash=_to_str(pw_hash), + password=password, + error=error, + ) + + +class MosquittoCredentialFinder(CredentialFinder): + REGEX_LIST = (re.compile(rb'[a-zA-Z][a-zA-Z0-9_-]{2,15}:\$6\$[a-zA-Z0-9+/=]+\$[a-zA-Z0-9+/]{86}=='),) + + @staticmethod + def _parse_entry(entry: bytes) -> CredentialResult: + user, _, _, salt_hash, passwd_hash, *_ = re.split(r'[:$]', _to_str(entry)) + passwd_entry = f'{user}:$dynamic_82${b64decode(passwd_hash).hex()}$HEX${b64decode(salt_hash).hex()}' + password, error = crack_hash(passwd_entry.encode(), '--format=dynamic_82') + return CredentialResult( + username=user, + full_entry=_to_str(entry), + type='mosquitto', + password_hash=passwd_entry, + password=password, + error=error, + ) + + +def _to_str(byte_str: bytes) -> str: + """ + result entries must be converted from `bytes` to `str` in order to be saved as JSON + """ + return byte_str.decode(errors='replace') diff --git a/src/plugins/analysis/users_and_passwords/test/test_plugin_password_file_analyzer.py b/src/plugins/analysis/users_and_passwords/test/test_plugin_password_file_analyzer.py index a48eaaa39..984846092 100644 --- a/src/plugins/analysis/users_and_passwords/test/test_plugin_password_file_analyzer.py +++ b/src/plugins/analysis/users_and_passwords/test/test_plugin_password_file_analyzer.py @@ -2,9 +2,8 @@ import pytest -from objects.file import FileObject - -from ..code.password_file_analyzer import AnalysisPlugin, crack_hash, parse_john_output +from ..code.password_file_analyzer import AnalysisPlugin +from ..internal.crack_password import _parse_john_output, crack_hash TEST_DATA_DIR = Path(__file__).parent / 'data' @@ -12,81 +11,66 @@ @pytest.mark.AnalysisPluginTestConfig(plugin_class=AnalysisPlugin) class TestAnalysisPluginPasswordFileAnalyzer: def test_process_object_shadow_file(self, analysis_plugin): - test_file = FileObject(file_path=str(TEST_DATA_DIR / 'passwd_test')) - processed_object = analysis_plugin.process_object(test_file) - results = processed_object.processed_analysis[analysis_plugin.NAME] - - assert len(results) == 15 - for item in [ - 'vboxadd:unix', - 'mongodb:unix', - 'clamav:unix', - 'pulse:unix', - 'johndoe:unix', - 'max:htpasswd', - 'test:mosquitto', - 'admin:htpasswd', - 'root:unix', - 'user:unix', - 'user2:unix', - 'nosalt:unix', + test_file = TEST_DATA_DIR / 'passwd_test' + with test_file.open() as fp: + result = analysis_plugin.analyze(fp, {}, {}) + summary = analysis_plugin.summarize(result) + + for user, type_, pw in [ + ('vboxadd', 'unix', None), + ('mongodb', 'unix', None), + ('clamav', 'unix', None), + ('pulse', 'unix', None), + ('johndoe', 'unix', '123456'), + ('max', 'http', 'dragon'), # MD5 apr1 + ('test', 'mosquitto', '123456'), + ('admin', 'http', 'admin'), # SHA-1 + ('root', 'unix', 'root'), # DES + ('user', 'unix', '1234'), # Blowfish / bcrypt + ('user2', 'unix', 'secret'), # MD5 + ('nosalt', 'unix', 'root'), # MD5 without salt ]: - assert item in results - assert item in results['summary'] - self._assert_pw_match(results, 'max:htpasswd', 'dragon') # MD5 apr1 - self._assert_pw_match(results, 'johndoe:unix', '123456') - self._assert_pw_match(results, 'test:mosquitto', '123456') - self._assert_pw_match(results, 'admin:htpasswd', 'admin') # SHA-1 - self._assert_pw_match(results, 'root:unix', 'root') # DES - self._assert_pw_match(results, 'user:unix', '1234') # Blowfish / bcrypt - self._assert_pw_match(results, 'user2:unix', 'secret') # MD5 - self._assert_pw_match(results, 'nosalt:unix', 'root') # MD5 without salt + assert any(i.username == user and i.type == type_ and i.password == pw for i in getattr(result, type_)) + assert f'{user}:{type_}' in summary def test_process_object_fp_file(self, analysis_plugin): - test_file = FileObject(file_path=str(TEST_DATA_DIR / 'passwd_FP_test')) - processed_object = analysis_plugin.process_object(test_file) - results = processed_object.processed_analysis[analysis_plugin.NAME] - assert len(results) == 1 - assert 'summary' in results - assert results['summary'] == [] + with (TEST_DATA_DIR / 'passwd_FP_test').open() as fp: + result = analysis_plugin.analyze(fp, {}, {}) + summary = analysis_plugin.summarize(result) + + assert len(result.unix) == 0 + assert len(result.http) == 0 + assert len(result.mosquitto) == 0 + assert summary == [] def test_process_object_password_in_binary_file(self, analysis_plugin): - test_file = FileObject(file_path=str(TEST_DATA_DIR / 'passwd.bin')) - processed_object = analysis_plugin.process_object(test_file) - results = processed_object.processed_analysis[analysis_plugin.NAME] - - assert len(results) == 4 - for item in ['johndoe:unix', 'max:htpasswd']: - assert item in results - assert item in results['summary'] - self._assert_pw_match(results, 'johndoe:unix', '123456') - self._assert_pw_match(results, 'max:htpasswd', 'dragon') - - @staticmethod - def _assert_pw_match(results: dict, key: str, pw: str): - user, type_ = key.split(':') - assert 'type' in results[key] - assert 'password-hash' in results[key] - assert 'password' in results[key] - assert results[key]['type'] == type_ - assert results[key]['password'] == pw - assert results['tags'][f'{user}_{pw}']['value'] == f'Password: {user}:{pw}' + with (TEST_DATA_DIR / 'passwd.bin').open() as fp: + result = analysis_plugin.analyze(fp, {}, {}) + summary = analysis_plugin.summarize(result) + + assert len(result.unix) == 1 + assert len(result.http) == 1 + for item in ['johndoe:unix', 'max:http']: + assert item in summary + for user, type_, pw in [ + ('johndoe', 'unix', '123456'), + ('max', 'http', 'dragon'), + ]: + assert any(i.username == user and i.type == type_ and i.password == pw for i in getattr(result, type_)) def test_crack_hash_failure(): passwd_entry = [b'user', b'BfKEUi/mdF1D2'] - result_entry = {} - assert crack_hash(b':'.join(passwd_entry[:2]), result_entry) is False - assert 'ERROR' in result_entry - assert result_entry['ERROR'] == 'password cracking not successful' + pw, error = crack_hash(b':'.join(passwd_entry[:2])) + assert pw is None + assert error == 'password cracking not successful' def test_hash_unsupported(): passwd_entry = [b'user', b'foobar'] - result_entry = {} - assert crack_hash(b':'.join(passwd_entry[:2]), result_entry) is False - assert 'ERROR' in result_entry - assert result_entry['ERROR'] == 'hash type is not supported' + pw, error = crack_hash(b':'.join(passwd_entry[:2])) + assert pw is None + assert error == 'hash type is not supported' def test_crack_hash_success(): @@ -94,10 +78,9 @@ def test_crack_hash_success(): 'test:$dynamic_82$2c93b2efec757302a527be320b005a935567f370f268a13936fa42ef331cc703' '6ec75a65f8112ce511ff6088c92a6fe1384fbd0f70a9bc7ac41aa6103384aa8c$HEX$010203040506' ) - result_entry = {} - assert crack_hash(passwd_entry.encode(), result_entry, '--format=dynamic_82') is True - assert 'password' in result_entry - assert result_entry['password'] == '123456' + pw, error = crack_hash(passwd_entry.encode(), '--format=dynamic_82') + assert error is None + assert pw == '123456' JOHN_FAIL_OUTPUT = 'No password hashes loaded (see FAQ)\n\n=== Results: ===\n0 password hashes cracked, 0 left' @@ -124,4 +107,4 @@ def test_crack_hash_success(): ], ) def test_parse_output(john_output, expected_result): - assert parse_john_output(john_output) == expected_result + assert _parse_john_output(john_output) == expected_result diff --git a/src/plugins/analysis/users_and_passwords/view/users_and_passwords.html b/src/plugins/analysis/users_and_passwords/view/users_and_passwords.html index 6c76b34a3..a8fe7dc8a 100644 --- a/src/plugins/analysis/users_and_passwords/view/users_and_passwords.html +++ b/src/plugins/analysis/users_and_passwords/view/users_and_passwords.html @@ -2,40 +2,54 @@ {% block analysis_result_details %} - {% set analysis_result = analysis_result | split_user_and_password_type %} - {% for key in analysis_result | sort %} + {% for type_, result_list in analysis_result.items() %} + {% if result_list %} - - {{ key }} - + {{ type_ }} Credentials: - - {% for password_type in analysis_result[key] %} + {% for item in result_list %} +
- + {% if item.password %} + + {% else %} + + {% endif %} - {% endfor %} -
- {{password_type}} - {{ item.username }}:{{ item.password }}{{ item.username }} - - {% for item in analysis_result[key][password_type] | sort %} - {% if item not in ["log", "cracked"] %} - - - - - {% endif%} - {% endfor %} +
- {{ item }} - - {{ analysis_result[key][password_type][item] }} -
+ + + + + {% if item.password %} + + + + + {% elif item.error %} + + + + + {% endif %} + + + + + {% if item.password_hash | length > 1 %} + + + + + {% endif %}
Username:{{ item.username }}
Password:{{ item.password }}
Error:{{ item.error }}
Full Entry:{{ item.full_entry }}
Password Hash:{{ item.password_hash }}
+ + {% endfor %} + {% endif %} {% endfor %} {% endblock %} diff --git a/src/test/unit/web_interface/test_app_jinja_filter_static.py b/src/test/unit/web_interface/test_app_jinja_filter_static.py index d178181fa..d8c08c6f6 100644 --- a/src/test/unit/web_interface/test_app_jinja_filter_static.py +++ b/src/test/unit/web_interface/test_app_jinja_filter_static.py @@ -3,15 +3,6 @@ from web_interface.components.jinja_filter import FilterClass -def test_split_user_and_password_type_entry(): - new_test_entry_form = {'test:mosquitto': {'password': '123456'}} - old_test_entry_form = {'test': {'password': '123456'}} - expected_new_entry = {'test': {'mosquitto': {'password': '123456'}}} - expected_old_entry = {'test': {'unix': {'password': '123456'}}} - assert expected_new_entry == FilterClass._split_user_and_password_type_entry(new_test_entry_form) - assert expected_old_entry == FilterClass._split_user_and_password_type_entry(old_test_entry_form) - - @pytest.mark.parametrize( ('hid', 'uid', 'current_uid', 'expected_output'), [ diff --git a/src/web_interface/components/jinja_filter.py b/src/web_interface/components/jinja_filter.py index e7fbd2754..e9c12e5f2 100644 --- a/src/web_interface/components/jinja_filter.py +++ b/src/web_interface/components/jinja_filter.py @@ -131,19 +131,6 @@ def _render_general_information_table( file_tree_paths=file_tree_paths, ) - @staticmethod - def _split_user_and_password_type_entry(result: dict): - new_result = {} - for key, value in result.items(): - if ':' in key: - *user_elements, password_type = key.split(':') - user = ':'.join(user_elements) - else: # for backward compatibility - user = key - password_type = 'unix' - new_result.setdefault(user, {})[password_type] = value - return new_result - def check_auth(self, _): return config.frontend.authentication.enabled @@ -234,7 +221,6 @@ def _setup_filters(self): ), 'sort_roles': flt.sort_roles_by_number_of_privileges, 'sort_users': flt.sort_users_by_name, - 'split_user_and_password_type': self._split_user_and_password_type_entry, 'str_to_hex': flt.str_to_hex, 'text_highlighter': flt.text_highlighter, 'uids_to_link': flt.uids_to_link,