diff --git a/secureli/actions/action.py b/secureli/actions/action.py index 1450d0c6..5709649b 100644 --- a/secureli/actions/action.py +++ b/secureli/actions/action.py @@ -12,7 +12,7 @@ from secureli.modules.core.core_services.scanner import ScannerService from secureli.modules.core.core_services.updater import UpdaterService -from secureli.modules.shared.utilities.formatter import format_sentence_list +from secureli.modules.shared.utilities import format_sentence_list class ActionDependencies: diff --git a/secureli/actions/scan.py b/secureli/actions/scan.py index 881ebf89..5c08db67 100644 --- a/secureli/actions/scan.py +++ b/secureli/actions/scan.py @@ -15,7 +15,7 @@ from secureli.modules.core.core_services.scanner import ScannerService from secureli.modules.shared.models.scan import ScanMode from secureli.settings import Settings -from secureli.modules.shared.utilities import usage_stats +from secureli.modules.shared import utilities ONE_WEEK_IN_SECONDS: int = 7 * 24 * 60 * 60 @@ -85,7 +85,7 @@ def publish_results( publish_results_condition == PublishResultsOption.ON_FAIL and not action_successful ): - result = usage_stats.post_log(log_str, Settings()) + result = utilities.post_log(log_str, Settings()) self.echo.debug(result.result_message) if result.result == Result.SUCCESS: @@ -137,7 +137,7 @@ def scan_repo( [ob.__dict__ for ob in scan_result.failures] ) - individual_failure_count = usage_stats.convert_failures_to_failure_count( + individual_failure_count = utilities.convert_failures_to_failure_count( scan_result.failures ) diff --git a/secureli/main.py b/secureli/main.py index 907ebb09..6cf8ff37 100644 --- a/secureli/main.py +++ b/secureli/main.py @@ -13,7 +13,7 @@ from secureli.modules.shared.resources import read_resource from secureli.settings import Settings import secureli.repositories.secureli_config as SecureliConfig -from secureli.modules.shared.utilities.secureli_meta import secureli_version +from secureli.modules.shared.utilities import secureli_version # Create SetupAction outside of DI, as it's not yet available. setup_action = SetupAction(epilog_template_data=read_resource("epilog.md")) diff --git a/secureli/modules/language_analyzer/language_config.py b/secureli/modules/language_analyzer/language_config.py index 7b23c964..4eb652c2 100644 --- a/secureli/modules/language_analyzer/language_config.py +++ b/secureli/modules/language_analyzer/language_config.py @@ -4,8 +4,7 @@ from secureli.modules.shared.models import language from secureli.modules.shared.resources.slugify import slugify -from secureli.modules.shared.utilities.hash import hash_config -from secureli.modules.shared.utilities.patterns import combine_patterns +from secureli.modules.shared.utilities import combine_patterns, hash_config class LanguageConfigService: diff --git a/secureli/modules/language_analyzer/language_support.py b/secureli/modules/language_analyzer/language_support.py index 5f0a975a..bad538cd 100644 --- a/secureli/modules/language_analyzer/language_support.py +++ b/secureli/modules/language_analyzer/language_support.py @@ -9,7 +9,7 @@ import secureli.repositories.secureli_config as SecureliConfig from secureli.modules.shared.abstractions.pre_commit import PreCommitAbstraction from secureli.modules.language_analyzer import git_ignore, language_config -from secureli.modules.shared.utilities.hash import hash_config +from secureli.modules.shared.utilities import hash_config class BuildConfigResult(pydantic.BaseModel): diff --git a/secureli/modules/observability/observability_services/logging.py b/secureli/modules/observability/observability_services/logging.py index ef8df714..dcdfad00 100644 --- a/secureli/modules/observability/observability_services/logging.py +++ b/secureli/modules/observability/observability_services/logging.py @@ -12,8 +12,7 @@ import secureli.repositories.secureli_config as SecureliConfig from secureli.modules.language_analyzer import language_support from secureli.repositories.secureli_config import SecureliConfigRepository -from secureli.modules.shared.utilities import git_meta -from secureli.modules.shared.utilities.secureli_meta import secureli_version +from secureli.modules.shared import utilities def generate_unique_id() -> str: @@ -21,7 +20,7 @@ def generate_unique_id() -> str: A unique identifier representing the log entry, including various bits specific to the user and environment """ - origin_email_branch = f"{git_meta.origin_url()}|{git_meta.git_user_email()}|{git_meta.current_branch_name()}" + origin_email_branch = f"{utilities.origin_url()}|{utilities.git_user_email()}|{utilities.current_branch_name()}" return f"{uuid4()}|{origin_email_branch}" @@ -43,9 +42,9 @@ class LogEntry(pydantic.BaseModel): id: str = generate_unique_id() timestamp: datetime = datetime.utcnow() - username: str = git_meta.git_user_email() + username: str = utilities.git_user_email() machineid: str = platform.uname().node - secureli_version: str = secureli_version() + secureli_version: str = utilities.secureli_version() languages: Optional[list[str]] status: LogStatus action: LogAction @@ -125,7 +124,7 @@ def failure( def _log(self, log_entry: LogEntry): """Commit a log entry to the branch log file""" log_folder_path = Path(SecureliConfig.FOLDER_PATH / ".secureli/logs") - path_to_log = log_folder_path / f"{git_meta.current_branch_name()}" + path_to_log = log_folder_path / f"{utilities.current_branch_name()}" # Do not simply mkdir the log folder path, in case the branch name contains # additional folder structure, like `bugfix/` or `feature/` diff --git a/secureli/modules/shared/abstractions/echo.py b/secureli/modules/shared/abstractions/echo.py index c93dc196..93d34493 100644 --- a/secureli/modules/shared/abstractions/echo.py +++ b/secureli/modules/shared/abstractions/echo.py @@ -1,12 +1,9 @@ from abc import ABC, abstractmethod -from enum import Enum from typing import IO, Optional import sys import typer -from secureli.modules.shared.models.echo import Color - -from secureli.modules.shared.utilities.logging import EchoLevel +from secureli.modules.shared.models.echo import Color, Level class EchoAbstraction(ABC): @@ -18,15 +15,15 @@ class EchoAbstraction(ABC): """ def __init__(self, level: str): - self.print_enabled = level != EchoLevel.off - self.debug_enabled = level == EchoLevel.debug - self.info_enabled = level in [EchoLevel.debug, EchoLevel.info] - self.warn_enabled = level in [EchoLevel.debug, EchoLevel.info, EchoLevel.warn] + self.print_enabled = level != Level.off + self.debug_enabled = level == Level.debug + self.info_enabled = level in [Level.debug, Level.info] + self.warn_enabled = level in [Level.debug, Level.info, Level.warn] self.error_enabled = level in [ - EchoLevel.debug, - EchoLevel.info, - EchoLevel.warn, - EchoLevel.error, + Level.debug, + Level.info, + Level.warn, + Level.error, ] @abstractmethod diff --git a/secureli/modules/shared/models/echo.py b/secureli/modules/shared/models/echo.py index 71f6423c..f3cfe531 100644 --- a/secureli/modules/shared/models/echo.py +++ b/secureli/modules/shared/models/echo.py @@ -15,3 +15,17 @@ class Color(str, Enum): MAGENTA = "magenta" CYAN = "cyan" WHITE = "white" + + +class Level(str, Enum): + debug = "DEBUG" + info = "INFO" + warn = "WARN" + error = "ERROR" + off = "OFF" + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return self.__str__() diff --git a/secureli/modules/shared/utilities.py b/secureli/modules/shared/utilities.py new file mode 100644 index 00000000..4af705d5 --- /dev/null +++ b/secureli/modules/shared/utilities.py @@ -0,0 +1,155 @@ +import configparser +import hashlib +import os +import requests +import subprocess + +from collections import Counter +from importlib.metadata import version +from typing import Optional + +from secureli.modules.observability.consts import logging +from secureli.modules.shared.models.publish_results import PublishLogResult +from secureli.modules.shared.models.result import Result +from secureli.modules.shared.models.scan import ScanFailure +from secureli.settings import Settings + + +def combine_patterns(patterns: list[str]) -> Optional[str]: + """ + Combines a set of patterns from pathspec into a single combined regex + suitable for use in circumstances that require a broad matching, such as + re.find_all or a pre-commit exclusion pattern. + + Calling this with an empty set of patterns will return None + :param patterns: The pattern strings provided by pathspec to combine + :return: a combined pattern containing the one or more patterns supplied, or None + if no patterns were supplied + """ + if not patterns: + return None + + if len(patterns) == 1: + return patterns[0] + + # Don't mutate the input + ignored_file_patterns = patterns.copy() + + # Quick sanitization process. Ultimately we combine all the regexes into a single + # entry, and all patterns are generated with a capture group with the same ID, which + # is invalid. We need to make sure every capture group is unique to combine them + # into a single expression. This is not my favorite. + for i in range(0, len(ignored_file_patterns)): + ignored_file_patterns[i] = str.replace( + ignored_file_patterns[i], "ps_d", f"ps_d{i}" + ) + combined_patterns = str.join("|", ignored_file_patterns) + combined_ignore_pattern = f"^({combined_patterns})$" + return combined_ignore_pattern + + +def convert_failures_to_failure_count(failure_list: list[ScanFailure]): + """ + Convert a list of Failure ids to a list of individual failure count with underscore naming convention + :param failure_list: a list of Failure Object + """ + list_of_failure_ids = [] + + for failure in failure_list: + failure_id_underscore = failure.id.replace("-", "_") + list_of_failure_ids.append(failure_id_underscore) + + failure_count_list = Counter(list_of_failure_ids) + return failure_count_list + + +def current_branch_name() -> str: + """Leverage the git HEAD file to determine the current branch name""" + try: + with open(".git/HEAD", "r") as f: + content = f.readlines() + for line in content: + if line[0:4] == "ref:": + return line.partition("refs/heads/")[2].strip() + except IOError: + return "UNKNOWN" + + +def format_sentence_list(items: list[str]) -> str: + """ + Formats a list of string values to a comma separated + string for use in a sentence structure. + :param items: list of strings to join + :return string of joined values as a sentence comma list + i.e. "x, y, and z" or "x and y" + """ + if not items: + return "" + elif len(items) == 1: + return items[0] + and_separator = ", and " if len(items) > 2 else " and " + return ", ".join(items[:-1]) + and_separator + items[-1] + + +def git_user_email() -> str: + """Leverage the command prompt to derive the user's email address""" + args = ["git", "config", "user.email"] + completed_process = subprocess.run(args, stdout=subprocess.PIPE) + output = completed_process.stdout.decode("utf8").strip() + return output + + +def hash_config(config: str) -> str: + """ + Creates an MD5 hash from a config string + :return: A hash string + """ + config_hash = hashlib.md5(config.encode("utf8"), usedforsecurity=False).hexdigest() + + return config_hash + + +def origin_url() -> str: + """Leverage the git config file to determine the remote origin URL""" + git_config_parser = configparser.ConfigParser() + git_config_parser.read(".git/config") + return ( + git_config_parser['remote "origin"'].get("url", "UNKNOWN", raw=True) + if git_config_parser.has_section('remote "origin"') + else "UNKNOWN" + ) + + +def post_log(log_data: str, settings: Settings) -> PublishLogResult: + """ + Send a log through http post + :param log_data: a string to be sent to backend instrumentation + """ + + api_endpoint = ( + os.getenv(logging.TELEMETRY_ENDPOINT_ENV_VAR_NAME) or settings.telemetry.api_url + ) + api_key = os.getenv(logging.TELEMETRY_KEY_ENV_VAR_NAME) + + if not api_endpoint or not api_key: + return PublishLogResult( + result=Result.FAILURE, + result_message=f"{logging.TELEMETRY_ENDPOINT_ENV_VAR_NAME} or {logging.TELEMETRY_KEY_ENV_VAR_NAME} not found in environment variables", + ) + + try: + result = requests.post( + url=api_endpoint, headers={"Api-Key": api_key}, data=log_data + ) + except Exception as e: + return PublishLogResult( + result=Result.FAILURE, + result_message=f'Error posting log to {api_endpoint}: "{e}"', + ) + + return PublishLogResult(result=Result.SUCCESS, result_message=result.text) + + +def secureli_version() -> str: + """Leverage package resources to determine the current version of secureli""" + return version("secureli") diff --git a/secureli/modules/shared/utilities/__init__.py b/secureli/modules/shared/utilities/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/secureli/modules/shared/utilities/formatter.py b/secureli/modules/shared/utilities/formatter.py deleted file mode 100644 index be7f9f71..00000000 --- a/secureli/modules/shared/utilities/formatter.py +++ /dev/null @@ -1,14 +0,0 @@ -def format_sentence_list(items: list[str]) -> str: - """ - Formats a list of string values to a comma separated - string for use in a sentence structure. - :param items: list of strings to join - :return string of joined values as a sentence comma list - i.e. "x, y, and z" or "x and y" - """ - if not items: - return "" - elif len(items) == 1: - return items[0] - and_separator = ", and " if len(items) > 2 else " and " - return ", ".join(items[:-1]) + and_separator + items[-1] diff --git a/secureli/modules/shared/utilities/git_meta.py b/secureli/modules/shared/utilities/git_meta.py deleted file mode 100644 index e15bc0c7..00000000 --- a/secureli/modules/shared/utilities/git_meta.py +++ /dev/null @@ -1,33 +0,0 @@ -import subprocess -import configparser - - -def git_user_email() -> str: - """Leverage the command prompt to derive the user's email address""" - args = ["git", "config", "user.email"] - completed_process = subprocess.run(args, stdout=subprocess.PIPE) - output = completed_process.stdout.decode("utf8").strip() - return output - - -def origin_url() -> str: - """Leverage the git config file to determine the remote origin URL""" - git_config_parser = configparser.ConfigParser() - git_config_parser.read(".git/config") - return ( - git_config_parser['remote "origin"'].get("url", "UNKNOWN", raw=True) - if git_config_parser.has_section('remote "origin"') - else "UNKNOWN" - ) - - -def current_branch_name() -> str: - """Leverage the git HEAD file to determine the current branch name""" - try: - with open(".git/HEAD", "r") as f: - content = f.readlines() - for line in content: - if line[0:4] == "ref:": - return line.partition("refs/heads/")[2].strip() - except IOError: - return "UNKNOWN" diff --git a/secureli/modules/shared/utilities/hash.py b/secureli/modules/shared/utilities/hash.py deleted file mode 100644 index 1af53f6d..00000000 --- a/secureli/modules/shared/utilities/hash.py +++ /dev/null @@ -1,11 +0,0 @@ -import hashlib - - -def hash_config(config: str) -> str: - """ - Creates an MD5 hash from a config string - :return: A hash string - """ - config_hash = hashlib.md5(config.encode("utf8"), usedforsecurity=False).hexdigest() - - return config_hash diff --git a/secureli/modules/shared/utilities/logging.py b/secureli/modules/shared/utilities/logging.py deleted file mode 100644 index 49642cae..00000000 --- a/secureli/modules/shared/utilities/logging.py +++ /dev/null @@ -1,15 +0,0 @@ -from enum import Enum - - -class EchoLevel(str, Enum): - debug = "DEBUG" - info = "INFO" - warn = "WARN" - error = "ERROR" - off = "OFF" - - def __str__(self) -> str: - return self.value - - def __repr__(self) -> str: - return self.__str__() diff --git a/secureli/modules/shared/utilities/patterns.py b/secureli/modules/shared/utilities/patterns.py deleted file mode 100644 index 0a845e39..00000000 --- a/secureli/modules/shared/utilities/patterns.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Optional - - -def combine_patterns(patterns: list[str]) -> Optional[str]: - """ - Combines a set of patterns from pathspec into a single combined regex - suitable for use in circumstances that require a broad matching, such as - re.find_all or a pre-commit exclusion pattern. - - Calling this with an empty set of patterns will return None - :param patterns: The pattern strings provided by pathspec to combine - :return: a combined pattern containing the one or more patterns supplied, or None - if no patterns were supplied - """ - if not patterns: - return None - - if len(patterns) == 1: - return patterns[0] - - # Don't mutate the input - ignored_file_patterns = patterns.copy() - - # Quick sanitization process. Ultimately we combine all the regexes into a single - # entry, and all patterns are generated with a capture group with the same ID, which - # is invalid. We need to make sure every capture group is unique to combine them - # into a single expression. This is not my favorite. - for i in range(0, len(ignored_file_patterns)): - ignored_file_patterns[i] = str.replace( - ignored_file_patterns[i], "ps_d", f"ps_d{i}" - ) - combined_patterns = str.join("|", ignored_file_patterns) - combined_ignore_pattern = f"^({combined_patterns})$" - return combined_ignore_pattern diff --git a/secureli/modules/shared/utilities/secureli_meta.py b/secureli/modules/shared/utilities/secureli_meta.py deleted file mode 100644 index 6478683e..00000000 --- a/secureli/modules/shared/utilities/secureli_meta.py +++ /dev/null @@ -1,6 +0,0 @@ -from importlib.metadata import version - - -def secureli_version() -> str: - """Leverage package resources to determine the current version of secureli""" - return version("secureli") diff --git a/secureli/modules/shared/utilities/usage_stats.py b/secureli/modules/shared/utilities/usage_stats.py deleted file mode 100644 index 6e2a9265..00000000 --- a/secureli/modules/shared/utilities/usage_stats.py +++ /dev/null @@ -1,54 +0,0 @@ -import requests -import os -from secureli.modules.observability.consts import logging -from secureli.modules.shared.models.publish_results import PublishLogResult -from secureli.modules.shared.models.result import Result -from collections import Counter -from secureli.modules.shared.models.scan import ScanFailure - -from secureli.settings import Settings - - -def convert_failures_to_failure_count(failure_list: list[ScanFailure]): - """ - Convert a list of Failure ids to a list of individual failure count with underscore naming convention - :param failure_list: a list of Failure Object - """ - list_of_failure_ids = [] - - for failure in failure_list: - failure_id_underscore = failure.id.replace("-", "_") - list_of_failure_ids.append(failure_id_underscore) - - failure_count_list = Counter(list_of_failure_ids) - return failure_count_list - - -def post_log(log_data: str, settings: Settings) -> PublishLogResult: - """ - Send a log through http post - :param log_data: a string to be sent to backend instrumentation - """ - - api_endpoint = ( - os.getenv(logging.TELEMETRY_ENDPOINT_ENV_VAR_NAME) or settings.telemetry.api_url - ) - api_key = os.getenv(logging.TELEMETRY_KEY_ENV_VAR_NAME) - - if not api_endpoint or not api_key: - return PublishLogResult( - result=Result.FAILURE, - result_message=f"{logging.TELEMETRY_ENDPOINT_ENV_VAR_NAME} or {logging.TELEMETRY_KEY_ENV_VAR_NAME} not found in environment variables", - ) - - try: - result = requests.post( - url=api_endpoint, headers={"Api-Key": api_key}, data=log_data - ) - except Exception as e: - return PublishLogResult( - result=Result.FAILURE, - result_message=f'Error posting log to {api_endpoint}: "{e}"', - ) - - return PublishLogResult(result=Result.SUCCESS, result_message=result.text) diff --git a/secureli/repositories/repo_files.py b/secureli/repositories/repo_files.py index 2d791c99..4679f9c5 100644 --- a/secureli/repositories/repo_files.py +++ b/secureli/repositories/repo_files.py @@ -2,7 +2,7 @@ import re import chardet -from secureli.modules.shared.utilities.patterns import combine_patterns +from secureli.modules.shared.utilities import combine_patterns class BinaryFileError(ValueError): diff --git a/secureli/repositories/repo_settings.py b/secureli/repositories/repo_settings.py index 893a0d2d..45075ea1 100644 --- a/secureli/repositories/repo_settings.py +++ b/secureli/repositories/repo_settings.py @@ -1,7 +1,7 @@ from pathlib import Path from typing import Optional from pydantic import BaseModel, BaseSettings, Field -from secureli.modules.shared.utilities.logging import EchoLevel +from secureli.modules.shared.models.echo import Level import yaml @@ -74,7 +74,7 @@ class EchoSettings(BaseSettings): Settings that affect how seCureLI provides information to the user. """ - level: EchoLevel = Field(default=EchoLevel.warn) + level: Level = Field(default=Level.warn) class LanguageSupportSettings(BaseSettings): @@ -157,7 +157,7 @@ def save(self, settings: SecureliFile): key: value for (key, value) in settings.dict().items() if value is not None } - # Converts EchoLevel to string + # Converts echo Level to string if settings_dict.get("echo"): settings_dict["echo"]["level"] = "{}".format(settings_dict["echo"]["level"]) diff --git a/tests/actions/test_scan_action.py b/tests/actions/test_scan_action.py index cb6c504e..88c78f82 100644 --- a/tests/actions/test_scan_action.py +++ b/tests/actions/test_scan_action.py @@ -2,6 +2,7 @@ from secureli.modules.shared.abstractions.pre_commit import RevisionPair from secureli.actions.action import ActionDependencies from secureli.actions.scan import ScanAction +from secureli.modules.shared.models.echo import Level from secureli.modules.shared.models.exit_codes import ExitCode from secureli.modules.shared.models.install import VerifyOutcome from secureli.modules.shared.models.language import AnalyzeResult @@ -75,7 +76,7 @@ def mock_get_time_far_from_epoch(mocker: MockerFixture) -> MagicMock: @pytest.fixture() def mock_default_settings(mock_settings_repository: MagicMock) -> MagicMock: - mock_echo_settings = repo_settings.EchoSettings(level=repo_settings.EchoLevel.info) + mock_echo_settings = repo_settings.EchoSettings(level=Level.info) mock_settings_file = repo_settings.SecureliFile(echo=mock_echo_settings) mock_settings_repository.load.return_value = mock_settings_file @@ -128,7 +129,7 @@ def scan_action( @pytest.fixture() def mock_post_log(mocker: MockerFixture) -> MagicMock: - return mocker.patch("secureli.modules.shared.utilities.usage_stats.post_log") + return mocker.patch("secureli.modules.shared.utilities.post_log") @mock.patch.dict(os.environ, {"API_KEY": "", "API_ENDPOINT": ""}, clear=True) diff --git a/tests/application/test_main.py b/tests/application/test_main.py index dce9b001..7832a4b6 100644 --- a/tests/application/test_main.py +++ b/tests/application/test_main.py @@ -10,7 +10,7 @@ from secureli.modules.shared.models.install import VerifyOutcome, VerifyResult from secureli.modules.shared.models.publish_results import PublishResultsOption from secureli.modules.shared.models.scan import ScanMode -from secureli.modules.shared.utilities.secureli_meta import secureli_version +from secureli.modules.shared.utilities import secureli_version @pytest.fixture() diff --git a/tests/modules/language_analyzer/test_language_support.py b/tests/modules/language_analyzer/test_language_support.py index 138f788c..d917c88c 100644 --- a/tests/modules/language_analyzer/test_language_support.py +++ b/tests/modules/language_analyzer/test_language_support.py @@ -37,7 +37,7 @@ def mock_hashlib(mocker: MockerFixture) -> MagicMock: mock_md5 = MagicMock() mock_hashlib.md5.return_value = mock_md5 mock_md5.hexdigest.return_value = "mock-hash-code" - mocker.patch("secureli.modules.shared.utilities.hash.hashlib", mock_hashlib) + mocker.patch("secureli.modules.shared.utilities.hashlib", mock_hashlib) return mock_hashlib @@ -47,7 +47,7 @@ def mock_hashlib_no_match(mocker: MockerFixture) -> MagicMock: mock_md5 = MagicMock() mock_hashlib.md5.return_value = mock_md5 mock_md5.hexdigest.side_effect = ["first-hash-code", "second-hash-code"] - mocker.patch("secureli.modules.shared.utilities.hash.hashlib", mock_hashlib) + mocker.patch("secureli.modules.shared.utilities.hashlib", mock_hashlib) return mock_hashlib diff --git a/tests/modules/shared/abstractions/test_pre_commit.py b/tests/modules/shared/abstractions/test_pre_commit.py index 73167181..a386ebf6 100644 --- a/tests/modules/shared/abstractions/test_pre_commit.py +++ b/tests/modules/shared/abstractions/test_pre_commit.py @@ -40,7 +40,7 @@ def mock_hashlib(mocker: MockerFixture) -> MagicMock: mock_md5 = MagicMock() mock_hashlib.md5.return_value = mock_md5 mock_md5.hexdigest.return_value = "mock-hash-code" - mocker.patch("secureli.modules.shared.utilities.hash.hashlib", mock_hashlib) + mocker.patch("secureli.modules.shared.utilities.hashlib", mock_hashlib) return mock_hashlib @@ -50,7 +50,7 @@ def mock_hashlib_no_match(mocker: MockerFixture) -> MagicMock: mock_md5 = MagicMock() mock_hashlib.md5.return_value = mock_md5 mock_md5.hexdigest.side_effect = ["first-hash-code", "second-hash-code"] - mocker.patch("secureli.modules.shared.utilities.hash.hashlib", mock_hashlib) + mocker.patch("secureli.modules.shared.utilities.hashlib", mock_hashlib) return mock_hashlib diff --git a/tests/modules/shared/abstractions/test_typer_echo.py b/tests/modules/shared/abstractions/test_typer_echo.py index 1ce201bd..6c089ab2 100644 --- a/tests/modules/shared/abstractions/test_typer_echo.py +++ b/tests/modules/shared/abstractions/test_typer_echo.py @@ -3,9 +3,7 @@ from unittest.mock import MagicMock, ANY import pytest -from secureli.modules.shared.models.echo import Color - -from secureli.modules.shared.utilities.logging import EchoLevel +from secureli.modules.shared.models.echo import Color, Level ECHO_SECURELI_PREFIX = "[seCureLI]" @@ -48,7 +46,7 @@ def typer_echo(request) -> TyperEcho: @pytest.mark.parametrize( "typer_echo", - [EchoLevel.info, EchoLevel.debug, EchoLevel.error, EchoLevel.warn], + [Level.info, Level.debug, Level.error, Level.warn], indirect=True, ) def test_that_typer_echo_renders_print_messages_correctly( @@ -68,10 +66,10 @@ def test_that_typer_echo_renders_print_messages_correctly( @pytest.mark.parametrize( "typer_echo", [ - EchoLevel.debug, - EchoLevel.info, - EchoLevel.warn, - EchoLevel.error, + Level.debug, + Level.info, + Level.warn, + Level.error, ], indirect=True, ) @@ -90,7 +88,7 @@ def test_that_typer_echo_renders_errors_correctly( @pytest.mark.parametrize( - "typer_echo", [EchoLevel.warn, EchoLevel.info, EchoLevel.debug], indirect=True + "typer_echo", [Level.warn, Level.info, Level.debug], indirect=True ) def test_that_typer_echo_renders_warnings_correctly( typer_echo: TyperEcho, @@ -106,7 +104,7 @@ def test_that_typer_echo_renders_warnings_correctly( ) -@pytest.mark.parametrize("typer_echo", [EchoLevel.info, EchoLevel.debug], indirect=True) +@pytest.mark.parametrize("typer_echo", [Level.info, Level.debug], indirect=True) def test_that_typer_echo_renders_info_correctly( typer_echo: TyperEcho, mock_echo_text: str, @@ -123,7 +121,7 @@ def test_that_typer_echo_renders_info_correctly( @pytest.mark.parametrize( "typer_echo", - [EchoLevel.debug], + [Level.debug], indirect=True, ) def test_that_typer_echo_renders_debug_messages_correctly( @@ -139,7 +137,7 @@ def test_that_typer_echo_renders_debug_messages_correctly( @pytest.mark.parametrize( "typer_echo", - [EchoLevel.off], + [Level.off], indirect=True, ) def test_that_typer_echo_suppresses_all_messages_when_off( @@ -158,7 +156,7 @@ def test_that_typer_echo_suppresses_all_messages_when_off( @pytest.mark.parametrize( "typer_echo", - [EchoLevel.off], + [Level.off], indirect=True, ) def test_that_typer_echo_suppresses_error_messages( @@ -170,7 +168,7 @@ def test_that_typer_echo_suppresses_error_messages( @pytest.mark.parametrize( "typer_echo", - [EchoLevel.error, EchoLevel.off], + [Level.error, Level.off], indirect=True, ) def test_that_typer_echo_suppresses_warning_messages( @@ -182,7 +180,7 @@ def test_that_typer_echo_suppresses_warning_messages( @pytest.mark.parametrize( "typer_echo", - [EchoLevel.warn, EchoLevel.error, EchoLevel.off], + [Level.warn, Level.error, Level.off], indirect=True, ) def test_that_typer_echo_suppresses_info_messages( @@ -194,7 +192,7 @@ def test_that_typer_echo_suppresses_info_messages( @pytest.mark.parametrize( "typer_echo", - [EchoLevel.info, EchoLevel.warn, EchoLevel.error, EchoLevel.off], + [Level.info, Level.warn, Level.error, Level.off], indirect=True, ) def test_that_typer_echo_suppresses_debug_messages( @@ -206,7 +204,7 @@ def test_that_typer_echo_suppresses_debug_messages( @pytest.mark.parametrize( "typer_echo", - [EchoLevel.info], + [Level.info], indirect=True, ) def test_that_typer_echo_prompts_user_for_confirmation( @@ -220,7 +218,7 @@ def test_that_typer_echo_prompts_user_for_confirmation( def test_that_typer_echo_implements_prompt_with_default(mock_typer_prompt: MagicMock): - typer_echo = TyperEcho(level=EchoLevel.info) + typer_echo = TyperEcho(level=Level.info) message = "test message" default_response = "default user response" typer_echo.prompt(message=message, default_response=default_response) @@ -233,7 +231,7 @@ def test_that_typer_echo_implements_prompt_with_default(mock_typer_prompt: Magic def test_that_typer_echo_implements_prompt_without_default( mock_typer_prompt: MagicMock, ): - typer_echo = TyperEcho(level=EchoLevel.info) + typer_echo = TyperEcho(level=Level.info) message = "test message" typer_echo.prompt(message=message) diff --git a/tests/modules/shared/utilities/test_formatter.py b/tests/modules/shared/utilities/test_formatter.py index 13bc7ae9..a1979e60 100644 --- a/tests/modules/shared/utilities/test_formatter.py +++ b/tests/modules/shared/utilities/test_formatter.py @@ -1,4 +1,4 @@ -from secureli.modules.shared.utilities.formatter import format_sentence_list +from secureli.modules.shared.utilities import format_sentence_list def test_format_sentence_list_handles_missing_list(): diff --git a/tests/modules/shared/utilities/test_git_meta.py b/tests/modules/shared/utilities/test_git_meta.py index 4123f7cd..94781e26 100644 --- a/tests/modules/shared/utilities/test_git_meta.py +++ b/tests/modules/shared/utilities/test_git_meta.py @@ -4,16 +4,14 @@ import pytest from pytest_mock import MockerFixture -from secureli.modules.shared.utilities import git_meta +from secureli.modules.shared import utilities mock_git_origin_url = r"git@github.com:my-org/repo%20with%20spaces.git" @pytest.fixture() def mock_subprocess(mocker: MockerFixture) -> MagicMock: - mock_subprocess = mocker.patch( - "secureli.modules.shared.utilities.git_meta.subprocess" - ) + mock_subprocess = mocker.patch("secureli.modules.shared.utilities.subprocess") mock_subprocess.run.return_value = CompletedProcess( args=[], returncode=0, stdout="great.engineer@slalom.com\n".encode("utf8") ) @@ -22,9 +20,7 @@ def mock_subprocess(mocker: MockerFixture) -> MagicMock: @pytest.fixture() def mock_configparser(mocker: MockerFixture) -> MagicMock: - mock_configparser = mocker.patch( - "secureli.modules.shared.utilities.git_meta.configparser" - ) + mock_configparser = mocker.patch("secureli.modules.shared.utilities.configparser") mock_configparser_instance = MagicMock() mock_configparser_instance['remote "origin"'].get.return_value = ( "https://fake-build.com/git/repo" @@ -61,14 +57,14 @@ def mock_open_io_error(mocker: MockerFixture) -> MagicMock: def test_git_user_email_loads_user_email_via_git_subprocess(mock_subprocess: MagicMock): - result = git_meta.git_user_email() + result = utilities.git_user_email() mock_subprocess.run.assert_called_once() assert result == "great.engineer@slalom.com" # note: without trailing newline def test_origin_url_parses_config_to_get_origin_url(mock_configparser: MagicMock): - result = git_meta.origin_url() + result = utilities.origin_url() mock_configparser.read.assert_called_once_with(".git/config") assert result == "https://fake-build.com/git/repo" @@ -77,7 +73,7 @@ def test_origin_url_parses_config_to_get_origin_url(mock_configparser: MagicMock def test_current_branch_name_finds_ref_name_from_head_file( mock_open_git_head: MagicMock, ): - result = git_meta.current_branch_name() + result = utilities.current_branch_name() assert result == "feature/wicked-sick-branch" @@ -85,10 +81,10 @@ def test_current_branch_name_finds_ref_name_from_head_file( def test_current_branch_name_yields_unknown_due_to_io_error( mock_open_io_error: MagicMock, ): - result = git_meta.current_branch_name() + result = utilities.current_branch_name() assert result == "UNKNOWN" def test_configparser_can_read_origin_url_with_percent(mock_open_git_origin: MagicMock): - assert git_meta.origin_url() == mock_git_origin_url + assert utilities.origin_url() == mock_git_origin_url diff --git a/tests/modules/shared/utilities/test_logging.py b/tests/modules/shared/utilities/test_logging.py index 7dce3346..bfbf5706 100644 --- a/tests/modules/shared/utilities/test_logging.py +++ b/tests/modules/shared/utilities/test_logging.py @@ -1,13 +1,13 @@ -from secureli.modules.shared.utilities.logging import EchoLevel +from secureli.modules.shared.models.echo import Level def test_that_echo_level_str_returns_enum_val(): - level = EchoLevel.info + level = Level.info assert str(level) == level.value def test_that_echo_level_repr_returns_str_implementation(): - level = EchoLevel.debug + level = Level.debug assert repr(level) == str(level) diff --git a/tests/modules/shared/utilities/test_patterns.py b/tests/modules/shared/utilities/test_patterns.py index dc5c9bd4..558839f4 100644 --- a/tests/modules/shared/utilities/test_patterns.py +++ b/tests/modules/shared/utilities/test_patterns.py @@ -1,4 +1,4 @@ -from secureli.modules.shared.utilities.patterns import combine_patterns +from secureli.modules.shared.utilities import combine_patterns def test_that_combine_patterns_returns_none_for_empty_list(): diff --git a/tests/modules/shared/utilities/test_usage_stats.py b/tests/modules/shared/utilities/test_usage_stats.py index b08e82c6..9f98e2c3 100644 --- a/tests/modules/shared/utilities/test_usage_stats.py +++ b/tests/modules/shared/utilities/test_usage_stats.py @@ -4,7 +4,7 @@ from secureli.modules.shared.models.scan import ScanFailure from secureli.repositories.repo_settings import TelemetrySettings from secureli.settings import Settings -from secureli.modules.shared.utilities import usage_stats +from secureli.modules.shared import utilities from unittest import mock from unittest.mock import Mock, patch @@ -18,7 +18,7 @@ def test_that_convert_failures_to_failure_count_returns_correct_count(): ScanFailure(id="testfailid2", file="testfile1", repo="testrepo1"), ] - result = usage_stats.convert_failures_to_failure_count(list_of_failure) + result = utilities.convert_failures_to_failure_count(list_of_failure) assert result["testfailid1"] == 2 assert result["testfailid2"] == 1 @@ -27,7 +27,7 @@ def test_that_convert_failures_to_failure_count_returns_correct_count(): def test_that_convert_failures_to_failure_count_returns_correctly_when_no_failure(): list_of_failure = [] - result = usage_stats.convert_failures_to_failure_count(list_of_failure) + result = utilities.convert_failures_to_failure_count(list_of_failure) assert result == {} @@ -42,7 +42,7 @@ def test_that_convert_failures_to_failure_count_returns_correctly_when_no_failur ) @patch("requests.post") def test_post_log_with_no_api_key(mock_requests): - result = usage_stats.post_log("testing", Settings()) + result = utilities.post_log("testing", Settings()) mock_requests.assert_not_called() @@ -63,7 +63,7 @@ def test_post_log_with_no_api_key(mock_requests): ) @patch("requests.post") def test_post_log_with_no_api_endpoint(mock_requests): - result = usage_stats.post_log( + result = utilities.post_log( "testing", Settings(telemetry=TelemetrySettings(api_url=None)) ) @@ -87,7 +87,7 @@ def test_post_log_with_no_api_endpoint(mock_requests): def test_post_log_http_error(mock_requests): mock_requests.side_effect = Exception("test exception") - result = usage_stats.post_log("test_log_data", Settings()) + result = utilities.post_log("test_log_data", Settings()) mock_requests.assert_called_once_with( url="testendpoint", headers={"Api-Key": "testkey"}, data="test_log_data" @@ -110,7 +110,7 @@ def test_post_log_http_error(mock_requests): def test_post_log_happy_path(mock_requests): mock_requests.return_value = Mock(status_code=202, text="sample-response") - result = usage_stats.post_log("test_log_data", Settings()) + result = utilities.post_log("test_log_data", Settings()) mock_requests.assert_called_once_with( url="testendpoint", headers={"Api-Key": "testkey"}, data="test_log_data" @@ -132,7 +132,7 @@ def test_post_log_happy_path(mock_requests): def test_post_log_uses_settings_endpoint_if_no_env_endpoint(mock_requests): mock_requests.return_value = Mock(status_code=202, text="sample-response") - result = usage_stats.post_log( + result = utilities.post_log( "test_log_data", Settings(telemetry=TelemetrySettings(api_url="testendpoint")) ) diff --git a/tests/repositories/test_settings_repository.py b/tests/repositories/test_settings_repository.py index 7ac889dc..76997e6c 100644 --- a/tests/repositories/test_settings_repository.py +++ b/tests/repositories/test_settings_repository.py @@ -4,6 +4,7 @@ from pytest_mock import MockerFixture from secureli.repositories import repo_settings +from secureli.modules.shared.models.echo import Level @pytest.fixture() @@ -67,7 +68,7 @@ def test_that_settings_file_loads_settings_when_present( ): secureli_file = settings_repository.load(existent_path) - assert secureli_file.echo.level == repo_settings.EchoLevel.error + assert secureli_file.echo.level == Level.error def test_that_settings_file_created_when_not_present( @@ -84,7 +85,7 @@ def test_that_repo_saves_config( mock_open: MagicMock, settings_repository: repo_settings.SecureliRepository, ): - echo_level = repo_settings.EchoSettings(level=repo_settings.EchoLevel.info) + echo_level = repo_settings.EchoSettings(level=Level.info) settings_file = repo_settings.SecureliFile(echo=echo_level) settings_repository.save(settings_file)