diff --git a/src/mvt/common/config.py b/src/mvt/common/config.py index 24bf4072..fc4b2ea5 100644 --- a/src/mvt/common/config.py +++ b/src/mvt/common/config.py @@ -12,7 +12,9 @@ from pydantic_settings import ( YamlConfigSettingsSource, ) -MVT_CONFIG_FOLDER = user_config_dir("mvt") +# MVT_CONFIG_FOLDER in the environment relocates the settings file, so that +# a test run or a scripted install never touches the user's own. +MVT_CONFIG_FOLDER = os.environ.get("MVT_CONFIG_FOLDER") or user_config_dir("mvt") MVT_CONFIG_PATH = os.path.join(MVT_CONFIG_FOLDER, "config.yaml") diff --git a/src/mvt/common/indicators.py b/src/mvt/common/indicators.py index 94afadf1..3886e34f 100644 --- a/src/mvt/common/indicators.py +++ b/src/mvt/common/indicators.py @@ -18,7 +18,9 @@ from appdirs import user_data_dir from .config import settings from .url import URL -MVT_DATA_FOLDER = user_data_dir("mvt") +# MVT_DATA_FOLDER in the environment relocates the downloaded indicators and +# the update-check state kept next to them. +MVT_DATA_FOLDER = os.environ.get("MVT_DATA_FOLDER") or user_data_dir("mvt") MVT_INDICATORS_FOLDER = os.path.join(MVT_DATA_FOLDER, "indicators") logger = logging.getLogger(__name__) @@ -71,7 +73,9 @@ class Indicators: if os.path.isfile(path) and path.lower().endswith(".stix2"): self.parse_stix2(path) elif os.path.isdir(path): - for file in glob.glob(os.path.join(path, "**", "*.stix2"), recursive=True): + for file in glob.glob( + os.path.join(path, "**", "*.stix2"), recursive=True + ): self.parse_stix2(file) else: self.log.error( @@ -518,9 +522,7 @@ class Indicators: the original URL order. """ batches = [list(urls) if urls else [] for urls in url_batches] - unique_urls = list( - dict.fromkeys(url for urls in batches for url in urls) - ) + unique_urls = list(dict.fromkeys(url for urls in batches for url in urls)) if not unique_urls: return [None] * len(batches) diff --git a/tests/conftest.py b/tests/conftest.py index 06a890a9..40a084a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,20 +3,26 @@ # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ +import atexit import logging import os +import shutil +import tempfile import pytest -from mvt.common.cli_plugins import ( - MVT_ANDROID_CUSTOM_COMMANDS_ENV, - MVT_CUSTOM_COMMANDS_ENV, - MVT_IOS_CUSTOM_COMMANDS_ENV, -) -from mvt.common.indicators import Indicators - from .artifacts.generate_stix import generate_test_stix_file +# The suite must neither read nor write the developer's own MVT settings, +# downloaded indicators or update-check state, and mvt.common.config saves +# the settings file as soon as it is imported. Both folders are redirected +# before any mvt module is imported, which is why this file imports none at +# the top; the subprocesses the tests start inherit the variables. +MVT_TEST_HOME = tempfile.mkdtemp(prefix="mvt-tests-") +atexit.register(shutil.rmtree, MVT_TEST_HOME, ignore_errors=True) +os.environ["MVT_CONFIG_FOLDER"] = os.path.join(MVT_TEST_HOME, "config") +os.environ["MVT_DATA_FOLDER"] = os.path.join(MVT_TEST_HOME, "data") + @pytest.fixture(scope="session", autouse=True) def indicator_file(request, tmp_path_factory): @@ -47,6 +53,8 @@ def indicators_factory(indicator_file): android_property_names=[], files_sha256=[], ): + from mvt.common.indicators import Indicators + ind = Indicators(log=logging.getLogger()) ind.parse_stix2(indicator_file) @@ -77,6 +85,11 @@ def restore_cli_commands(monkeypatch): """ from mvt.android.cli import cli as android_cli from mvt.cli import cli as neutral_cli + from mvt.common.cli_plugins import ( + MVT_ANDROID_CUSTOM_COMMANDS_ENV, + MVT_CUSTOM_COMMANDS_ENV, + MVT_IOS_CUSTOM_COMMANDS_ENV, + ) from mvt.ios.cli import cli as ios_cli groups = (neutral_cli, ios_cli, android_cli)