From 5e60bb3b2765e0fff17bca3359282844fb9af8cf Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 12 Dec 2025 13:17:54 -0800 Subject: [PATCH 01/11] Updating initialize_async to support supplied env files --- pyrit/cli/frontend_core.py | 73 +++++++++---- pyrit/cli/pyrit_scan.py | 22 ++++ pyrit/cli/pyrit_shell.py | 34 ++++++ pyrit/setup/initialization.py | 64 +++++------ tests/unit/setup/test_initialization.py | 136 +++++++++++++++++++++++- 5 files changed, 280 insertions(+), 49 deletions(-) diff --git a/pyrit/cli/frontend_core.py b/pyrit/cli/frontend_core.py index 0b67ac0424..907de69d79 100644 --- a/pyrit/cli/frontend_core.py +++ b/pyrit/cli/frontend_core.py @@ -77,6 +77,7 @@ def __init__( database: str = SQLITE, initialization_scripts: Optional[list[Path]] = None, initializer_names: Optional[list[str]] = None, + env_files: Optional[list[Path]] = None, log_level: str = "WARNING", ): """ @@ -86,6 +87,7 @@ def __init__( database: Database type (InMemory, SQLite, or AzureSQL). initialization_scripts: Optional list of initialization script paths. initializer_names: Optional list of built-in initializer names to run. + env_files: Optional list of environment file paths to load in order. log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). Defaults to WARNING. Raises: @@ -95,6 +97,7 @@ def __init__( self._database = validate_database(database=database) self._initialization_scripts = initialization_scripts self._initializer_names = initializer_names + self._env_files = env_files self._log_level = validate_log_level(log_level=log_level) # Lazy-loaded registries @@ -119,6 +122,7 @@ async def initialize_async(self) -> None: memory_db_type=self._database, initialization_scripts=None, initializers=None, + env_files=self._env_files, ) # Load registries @@ -259,6 +263,7 @@ async def run_scenario_async( memory_db_type=context._database, initialization_scripts=context._initialization_scripts, initializers=initializer_instances, + env_files=context._env_files, ) # Get scenario class @@ -557,6 +562,46 @@ def wrapper(value): return wrapper +def resolve_initialization_scripts(script_paths: list[str]) -> list[Path]: + """ + Resolve initialization script paths. + + Args: + script_paths: List of script path strings. + + Returns: + List of resolved Path objects. + + Raises: + FileNotFoundError: If a script path does not exist. + """ + from pyrit.cli.initializer_registry import InitializerRegistry + + return InitializerRegistry.resolve_script_paths(script_paths=script_paths) + + +def resolve_env_files(*, env_file_paths: list[str]) -> list[Path]: + """ + Resolve environment file paths to absolute Path objects. + + Args: + env_file_paths: List of environment file path strings. + + Returns: + List of resolved Path objects. + + Raises: + ValueError: If any path does not exist. + """ + resolved_paths = [] + for path_str in env_file_paths: + path = Path(path_str).resolve() + if not path.exists(): + raise ValueError(f"Environment file not found: {path}") + resolved_paths.append(path) + return resolved_paths + + # Argparse-compatible validators # # These wrappers adapt our core validators (which use keyword-only parameters and raise @@ -573,6 +618,7 @@ def wrapper(value): validate_log_level_argparse = _argparse_validator(validate_log_level) positive_int = _argparse_validator(lambda v: validate_integer(v, min_value=1)) non_negative_int = _argparse_validator(lambda v: validate_integer(v, min_value=0)) +resolve_env_files_argparse = _argparse_validator(resolve_env_files) def parse_memory_labels(json_string: str) -> dict[str, str]: @@ -604,24 +650,6 @@ def parse_memory_labels(json_string: str) -> dict[str, str]: return labels -def resolve_initialization_scripts(script_paths: list[str]) -> list[Path]: - """ - Resolve initialization script paths. - - Args: - script_paths: List of script path strings. - - Returns: - List of resolved Path objects. - - Raises: - FileNotFoundError: If a script path does not exist. - """ - from pyrit.cli.initializer_registry import InitializerRegistry - - return InitializerRegistry.resolve_script_paths(script_paths=script_paths) - - def get_default_initializer_discovery_path() -> Path: """ Get the default path for discovering initializers. @@ -688,6 +716,7 @@ async def print_initializers_list_async(*, context: FrontendCore, discovery_path ARG_HELP = { "initializers": "Built-in initializer names to run before the scenario (e.g., openai_objective_target)", "initialization_scripts": "Paths to custom Python initialization scripts to run before the scenario", + "env_files": "Paths to environment files to load in order (e.g., .env.production .env.local). Later files override earlier ones.", "scenario_strategies": "List of strategy names to run (e.g., base64 rot13)", "max_concurrency": "Maximum number of concurrent attack executions (must be >= 1)", "max_retries": "Maximum number of automatic retries on exception (must be >= 0)", @@ -728,6 +757,7 @@ def parse_run_arguments(*, args_string: str) -> dict[str, Any]: "scenario_name": parts[0], "initializers": None, "initialization_scripts": None, + "env_files": None, "scenario_strategies": None, "max_concurrency": None, "max_retries": None, @@ -752,6 +782,13 @@ def parse_run_arguments(*, args_string: str) -> dict[str, Any]: while i < len(parts) and not parts[i].startswith("--"): result["initialization_scripts"].append(parts[i]) i += 1 + elif parts[i] == "--env-files": + # Collect env file paths until next flag + result["env_files"] = [] + i += 1 + while i < len(parts) and not parts[i].startswith("--"): + result["env_files"].append(parts[i]) + i += 1 elif parts[i] in ("--strategies", "-s"): # Collect strategies until next flag result["scenario_strategies"] = [] diff --git a/pyrit/cli/pyrit_scan.py b/pyrit/cli/pyrit_scan.py index 9cb1affa11..19d9d8535d 100644 --- a/pyrit/cli/pyrit_scan.py +++ b/pyrit/cli/pyrit_scan.py @@ -94,6 +94,13 @@ def parse_args(args=None) -> Namespace: help=frontend_core.ARG_HELP["initialization_scripts"], ) + parser.add_argument( + "--env-files", + type=str, + nargs="+", + help=frontend_core.ARG_HELP["env_files"], + ) + parser.add_argument( "--strategies", "-s", @@ -152,9 +159,18 @@ def main(args=None) -> int: print(f"Error: {e}") return 1 + env_files = None + if parsed_args.env_files: + try: + env_files = frontend_core.resolve_env_files(env_file_paths=parsed_args.env_files) + except ValueError as e: + print(f"Error: {e}") + return 1 + context = frontend_core.FrontendCore( database=parsed_args.database, initialization_scripts=initialization_scripts, + env_files=env_files, log_level=parsed_args.log_level, ) @@ -181,11 +197,17 @@ def main(args=None) -> int: script_paths=parsed_args.initialization_scripts ) + # Collect environment files + env_files = None + if parsed_args.env_files: + env_files = frontend_core.resolve_env_files(env_file_paths=parsed_args.env_files) + # Create context with initializers context = frontend_core.FrontendCore( database=parsed_args.database, initialization_scripts=initialization_scripts, initializer_names=parsed_args.initializers, + env_files=env_files, log_level=parsed_args.log_level, ) diff --git a/pyrit/cli/pyrit_shell.py b/pyrit/cli/pyrit_shell.py index 6ae915781d..d149037c78 100644 --- a/pyrit/cli/pyrit_shell.py +++ b/pyrit/cli/pyrit_shell.py @@ -39,10 +39,12 @@ class PyRITShell(cmd.Cmd): Shell Startup Options: --database Database type (InMemory, SQLite, AzureSQL) - default for all runs --log-level Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) - default for all runs + --env-files ... Environment files to load in order - default for all runs Run Command Options: --initializers ... Built-in initializers to run before the scenario --initialization-scripts <...> Custom Python scripts to run before the scenario + --env-files ... Environment files to load in order (overrides startup default) --strategies, -s ... Strategy names to use --max-concurrency Maximum concurrent operations --max-retries Maximum retry attempts @@ -97,6 +99,7 @@ def __init__( self.context = context self.default_database = context._database self.default_log_level = context._log_level + self.default_env_files = context._env_files # Track scenario execution history: list of (command_string, ScenarioResult) tuples self._scenario_history: list[tuple[str, ScenarioResult]] = [] @@ -150,6 +153,7 @@ def do_run(self, line): Options: --initializers ... Built-in initializers to run before the scenario --initialization-scripts <...> Custom Python scripts to run before the scenario + --env-files ... Environment files to load in order --strategies, -s ... Strategy names to use --max-concurrency Maximum concurrent operations --max-retries Maximum retry attempts @@ -214,11 +218,24 @@ def do_run(self, line): print(f"Error: {e}") return + # Resolve env files if provided + resolved_env_files = None + if args["env_files"]: + try: + resolved_env_files = frontend_core.resolve_env_files(env_file_paths=args["env_files"]) + except ValueError as e: + print(f"Error: {e}") + return + else: + # Use default env files from shell startup + resolved_env_files = self.default_env_files + # Create a context for this run with overrides run_context = frontend_core.FrontendCore( database=args["database"] or self.default_database, initialization_scripts=resolved_scripts, initializer_names=args["initializers"], + env_files=resolved_env_files, log_level=args["log_level"] or self.default_log_level, ) # Use the existing registries (don't reinitialize) @@ -455,13 +472,30 @@ def main(): help="Default logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) (default: WARNING, can be overridden per-run)", ) + parser.add_argument( + "--env-files", + type=str, + nargs="+", + help="Environment files to load in order (default for all runs, can be overridden per-run)", + ) + args = parser.parse_args() + # Resolve env files if provided + env_files = None + if args.env_files: + try: + env_files = frontend_core.resolve_env_files(env_file_paths=args.env_files) + except ValueError as e: + print(f"Error: {e}") + return 1 + # Create context (initializers are specified per-run, not at startup) context = frontend_core.FrontendCore( database=args.database, initialization_scripts=None, initializer_names=None, + env_files=env_files, log_level=args.log_level, ) diff --git a/pyrit/setup/initialization.py b/pyrit/setup/initialization.py index ba16b28334..f3a88dcd08 100644 --- a/pyrit/setup/initialization.py +++ b/pyrit/setup/initialization.py @@ -28,27 +28,37 @@ MemoryDatabaseType = Literal["InMemory", "SQLite", "AzureSQL"] -def _load_environment_files() -> None: +def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]]) -> None: """ - Load the base environment file from .env if it exists, - then load a single .env.local file if it exists, overriding previous values. + Load environment files in the order they are provided. + Later files override values from earlier files. """ - base_file_path = path.HOME_PATH / ".env" - local_file_path = path.HOME_PATH / ".env.local" - # Load the base .env file if it exists - if base_file_path.exists(): - dotenv.load_dotenv(base_file_path, override=True, interpolate=True) - logger.info(f"Loaded {base_file_path}") - else: - dotenv.load_dotenv(verbose=True) - - # Load the .env.local file if it exists, to override base .env values - if local_file_path.exists(): - dotenv.load_dotenv(local_file_path, override=True, interpolate=True) - logger.info(f"Loaded {local_file_path}") - else: - dotenv.load_dotenv(dotenv_path=dotenv.find_dotenv(".env.local"), override=True, verbose=True) + # Validate env_files exist if they were provided + if env_files is not None: + for env_file in env_files: + if not env_file.exists(): + raise ValueError(f"Environment file not found: {env_file}") + + # By default load .env and .env.local from home directory of the package + if env_files is None: + default_files = [] + base_file = path.HOME_PATH / ".env" + local_file = path.HOME_PATH / ".env.local" + + if base_file.exists(): + default_files.append(base_file) + if local_file.exists(): + default_files.append(local_file) + + env_files = default_files + + for env_file in env_files: + if env_file.exists(): + dotenv.load_dotenv(env_file, override=True, interpolate=True) + logger.info(f"Loaded {env_file}") + else: + logger.warning(f"Environment file not found: {env_file}") def _load_initializers_from_scripts( @@ -189,6 +199,7 @@ async def initialize_pyrit_async( *, initialization_scripts: Optional[Sequence[Union[str, pathlib.Path]]] = None, initializers: Optional[Sequence["PyRITInitializer"]] = None, + env_files: Optional[Sequence[pathlib.Path]] = None, **memory_instance_kwargs: Any, ) -> None: """ @@ -202,23 +213,16 @@ async def initialize_pyrit_async( or an 'initializers' variable that returns/contains a list of PyRITInitializer instances. initializers (Optional[Sequence[PyRITInitializer]]): Optional sequence of PyRITInitializer instances to execute directly. These provide type-safe, validated configuration with clear documentation. + env_files (Optional[Sequence[pathlib.Path]]): Optional sequence of environment file paths to load + in order. If not provided, will load default .env and .env.local files from PyRIT home if they exist. + All paths must be valid pathlib.Path objects. **memory_instance_kwargs (Optional[Any]): Additional keyword arguments to pass to the memory instance. Raises: - ValueError: If an unsupported memory_db_type is provided. + ValueError: If an unsupported memory_db_type is provided or if env_files contains non-existent files. """ - # Handle DuckDB deprecation before validation - if memory_db_type == "DuckDB": - logger.warning( - "DuckDB is no longer supported and has been replaced by SQLite for better compatibility and performance. " - "Please update your code to use SQLite instead. " - "For migration guidance, see the SQLite Memory documentation at: " - "doc/code/memory/1_sqlite_memory.ipynb. " - "Using in-memory SQLite instead." - ) - memory_db_type = IN_MEMORY - _load_environment_files() + _load_environment_files(env_files=env_files) # Reset all default values before executing initialization scripts # This ensures a clean state for each initialization diff --git a/tests/unit/setup/test_initialization.py b/tests/unit/setup/test_initialization.py index ce67f1e41d..8f567ad7ab 100644 --- a/tests/unit/setup/test_initialization.py +++ b/tests/unit/setup/test_initialization.py @@ -2,6 +2,7 @@ # Licensed under the MIT license. import os +import pathlib import tempfile from unittest import mock @@ -9,7 +10,7 @@ from pyrit.common.apply_defaults import reset_default_values from pyrit.setup import IN_MEMORY, initialize_pyrit_async -from pyrit.setup.initialization import _load_initializers_from_scripts +from pyrit.setup.initialization import _load_environment_files, _load_initializers_from_scripts class TestLoadInitializersFromScripts: @@ -104,3 +105,136 @@ async def test_invalid_memory_type_raises_error(self): """Test that invalid memory type raises ValueError.""" with pytest.raises(ValueError, match="is not a supported type"): await initialize_pyrit_async(memory_db_type="InvalidType") # type: ignore + + +class TestLoadEnvironmentFiles: + """Tests for _load_environment_files function and env_files parameter in initialize_pyrit_async.""" + + @pytest.mark.asyncio + @mock.patch("pyrit.setup.initialization.dotenv.load_dotenv") + @mock.patch("pyrit.setup.initialization.path.HOME_PATH") + async def test_loads_default_env_files_when_none_provided(self, mock_home_path, mock_load_dotenv): + """Test that default .env and .env.local files are loaded when env_files is None.""" + # Create temporary directory and files + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = pathlib.Path(temp_dir) + env_file = temp_path / ".env" + env_local_file = temp_path / ".env.local" + + # Create the files + env_file.write_text("VAR1=value1") + env_local_file.write_text("VAR2=value2") + + # Mock HOME_PATH to point to our temp directory + mock_home_path.__truediv__ = lambda self, other: temp_path / other + + # Call the function with None (default behavior) + _load_environment_files(env_files=None) + + # Verify both files were loaded + assert mock_load_dotenv.call_count == 2 + calls = [call[0][0] for call in mock_load_dotenv.call_args_list] + assert env_file in calls + assert env_local_file in calls + + @pytest.mark.asyncio + @mock.patch("pyrit.setup.initialization.dotenv.load_dotenv") + @mock.patch("pyrit.setup.initialization.path.HOME_PATH") + async def test_only_loads_existing_default_files(self, mock_home_path, mock_load_dotenv): + """Test that only existing default files are loaded.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = pathlib.Path(temp_dir) + env_file = temp_path / ".env" + + # Only create .env, not .env.local + env_file.write_text("VAR1=value1") + + mock_home_path.__truediv__ = lambda self, other: temp_path / other + + _load_environment_files(env_files=None) + + # Verify only one file was loaded + assert mock_load_dotenv.call_count == 1 + assert mock_load_dotenv.call_args[0][0] == env_file + + @pytest.mark.asyncio + @mock.patch("pyrit.setup.initialization.dotenv.load_dotenv") + async def test_loads_custom_env_files_in_order(self, mock_load_dotenv): + """Test that custom env_files are loaded in the order provided.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = pathlib.Path(temp_dir) + env1 = temp_path / ".env.test" + env2 = temp_path / ".env.prod" + env3 = temp_path / ".env.local" + + # Create files + env1.write_text("VAR=test") + env2.write_text("VAR=prod") + env3.write_text("VAR=local") + + # Pass custom files + _load_environment_files(env_files=[env1, env2, env3]) + + # Verify all three files were loaded in order + assert mock_load_dotenv.call_count == 3 + call_args = [call[0][0] for call in mock_load_dotenv.call_args_list] + assert call_args == [env1, env2, env3] + + @pytest.mark.asyncio + async def test_raises_error_for_nonexistent_env_file(self): + """Test that ValueError is raised for non-existent env file.""" + nonexistent = pathlib.Path("/nonexistent/path/.env") + + with pytest.raises(ValueError, match="Environment file not found"): + _load_environment_files(env_files=[nonexistent]) + + @pytest.mark.asyncio + @mock.patch("pyrit.memory.central_memory.CentralMemory.set_memory_instance") + async def test_initialize_pyrit_with_custom_env_files(self, mock_set_memory): + """Test initialize_pyrit_async with custom env_files.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = pathlib.Path(temp_dir) + env_file = temp_path / ".env.custom" + env_file.write_text("CUSTOM_VAR=custom_value") + + # Should not raise an error + await initialize_pyrit_async(memory_db_type=IN_MEMORY, env_files=[env_file]) + + mock_set_memory.assert_called_once() + + @pytest.mark.asyncio + @mock.patch("pyrit.memory.central_memory.CentralMemory.set_memory_instance") + async def test_initialize_pyrit_raises_for_nonexistent_env_file(self, mock_set_memory): + """Test that initialize_pyrit_async raises ValueError for non-existent env file.""" + nonexistent = pathlib.Path("/nonexistent/.env") + + with pytest.raises(ValueError, match="Environment file not found"): + await initialize_pyrit_async(memory_db_type=IN_MEMORY, env_files=[nonexistent]) + + @pytest.mark.asyncio + @mock.patch("pyrit.setup.initialization.dotenv.load_dotenv") + @mock.patch("pyrit.setup.initialization.path.HOME_PATH") + @mock.patch("pyrit.memory.central_memory.CentralMemory.set_memory_instance") + async def test_custom_env_files_override_default_behavior(self, mock_set_memory, mock_home_path, mock_load_dotenv): + """Test that passing custom env_files prevents loading default files.""" + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = pathlib.Path(temp_dir) + + # Create default files + default_env = temp_path / ".env" + default_env_local = temp_path / ".env.local" + default_env.write_text("DEFAULT=value") + default_env_local.write_text("DEFAULT_LOCAL=value") + + # Create custom file + custom_env = temp_path / ".env.custom" + custom_env.write_text("CUSTOM=value") + + mock_home_path.__truediv__ = lambda self, other: temp_path / other + + # Pass custom env_files - should NOT load defaults + await initialize_pyrit_async(memory_db_type=IN_MEMORY, env_files=[custom_env]) + + # Verify only custom file was loaded, not the default ones + assert mock_load_dotenv.call_count == 1 + assert mock_load_dotenv.call_args[0][0] == custom_env From 18b53fafbbe4979d3caba07e8409941c4f20f6dd Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 12 Dec 2025 13:19:52 -0800 Subject: [PATCH 02/11] pre-commit --- pyrit/cli/frontend_core.py | 3 +- pyrit/setup/initialization.py | 13 ++++--- tests/unit/setup/test_initialization.py | 45 +++++++++++++------------ 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/pyrit/cli/frontend_core.py b/pyrit/cli/frontend_core.py index 907de69d79..d09ae87b9c 100644 --- a/pyrit/cli/frontend_core.py +++ b/pyrit/cli/frontend_core.py @@ -716,7 +716,8 @@ async def print_initializers_list_async(*, context: FrontendCore, discovery_path ARG_HELP = { "initializers": "Built-in initializer names to run before the scenario (e.g., openai_objective_target)", "initialization_scripts": "Paths to custom Python initialization scripts to run before the scenario", - "env_files": "Paths to environment files to load in order (e.g., .env.production .env.local). Later files override earlier ones.", + "env_files": "Paths to environment files to load in order (e.g., .env.production .env.local). Later files " + "override earlier ones.", "scenario_strategies": "List of strategy names to run (e.g., base64 rot13)", "max_concurrency": "Maximum number of concurrent attack executions (must be >= 1)", "max_retries": "Maximum number of automatic retries on exception (must be >= 0)", diff --git a/pyrit/setup/initialization.py b/pyrit/setup/initialization.py index f3a88dcd08..0acaf222c3 100644 --- a/pyrit/setup/initialization.py +++ b/pyrit/setup/initialization.py @@ -32,8 +32,14 @@ def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]]) -> None """ Load environment files in the order they are provided. Later files override values from earlier files. - """ + Args: + env_files: Optional sequence of environment file paths. If None, loads default + .env and .env.local from PyRIT home directory (only if they exist). + + Raises: + ValueError: If any provided env_files do not exist. + """ # Validate env_files exist if they were provided if env_files is not None: for env_file in env_files: @@ -45,12 +51,12 @@ def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]]) -> None default_files = [] base_file = path.HOME_PATH / ".env" local_file = path.HOME_PATH / ".env.local" - + if base_file.exists(): default_files.append(base_file) if local_file.exists(): default_files.append(local_file) - + env_files = default_files for env_file in env_files: @@ -221,7 +227,6 @@ async def initialize_pyrit_async( Raises: ValueError: If an unsupported memory_db_type is provided or if env_files contains non-existent files. """ - _load_environment_files(env_files=env_files) # Reset all default values before executing initialization scripts diff --git a/tests/unit/setup/test_initialization.py b/tests/unit/setup/test_initialization.py index 8f567ad7ab..3b4d467fd4 100644 --- a/tests/unit/setup/test_initialization.py +++ b/tests/unit/setup/test_initialization.py @@ -10,7 +10,10 @@ from pyrit.common.apply_defaults import reset_default_values from pyrit.setup import IN_MEMORY, initialize_pyrit_async -from pyrit.setup.initialization import _load_environment_files, _load_initializers_from_scripts +from pyrit.setup.initialization import ( + _load_environment_files, + _load_initializers_from_scripts, +) class TestLoadInitializersFromScripts: @@ -120,17 +123,17 @@ async def test_loads_default_env_files_when_none_provided(self, mock_home_path, temp_path = pathlib.Path(temp_dir) env_file = temp_path / ".env" env_local_file = temp_path / ".env.local" - + # Create the files env_file.write_text("VAR1=value1") env_local_file.write_text("VAR2=value2") - + # Mock HOME_PATH to point to our temp directory mock_home_path.__truediv__ = lambda self, other: temp_path / other - + # Call the function with None (default behavior) _load_environment_files(env_files=None) - + # Verify both files were loaded assert mock_load_dotenv.call_count == 2 calls = [call[0][0] for call in mock_load_dotenv.call_args_list] @@ -145,14 +148,14 @@ async def test_only_loads_existing_default_files(self, mock_home_path, mock_load with tempfile.TemporaryDirectory() as temp_dir: temp_path = pathlib.Path(temp_dir) env_file = temp_path / ".env" - + # Only create .env, not .env.local env_file.write_text("VAR1=value1") - + mock_home_path.__truediv__ = lambda self, other: temp_path / other - + _load_environment_files(env_files=None) - + # Verify only one file was loaded assert mock_load_dotenv.call_count == 1 assert mock_load_dotenv.call_args[0][0] == env_file @@ -166,15 +169,15 @@ async def test_loads_custom_env_files_in_order(self, mock_load_dotenv): env1 = temp_path / ".env.test" env2 = temp_path / ".env.prod" env3 = temp_path / ".env.local" - + # Create files env1.write_text("VAR=test") env2.write_text("VAR=prod") env3.write_text("VAR=local") - + # Pass custom files _load_environment_files(env_files=[env1, env2, env3]) - + # Verify all three files were loaded in order assert mock_load_dotenv.call_count == 3 call_args = [call[0][0] for call in mock_load_dotenv.call_args_list] @@ -184,7 +187,7 @@ async def test_loads_custom_env_files_in_order(self, mock_load_dotenv): async def test_raises_error_for_nonexistent_env_file(self): """Test that ValueError is raised for non-existent env file.""" nonexistent = pathlib.Path("/nonexistent/path/.env") - + with pytest.raises(ValueError, match="Environment file not found"): _load_environment_files(env_files=[nonexistent]) @@ -196,10 +199,10 @@ async def test_initialize_pyrit_with_custom_env_files(self, mock_set_memory): temp_path = pathlib.Path(temp_dir) env_file = temp_path / ".env.custom" env_file.write_text("CUSTOM_VAR=custom_value") - + # Should not raise an error await initialize_pyrit_async(memory_db_type=IN_MEMORY, env_files=[env_file]) - + mock_set_memory.assert_called_once() @pytest.mark.asyncio @@ -207,7 +210,7 @@ async def test_initialize_pyrit_with_custom_env_files(self, mock_set_memory): async def test_initialize_pyrit_raises_for_nonexistent_env_file(self, mock_set_memory): """Test that initialize_pyrit_async raises ValueError for non-existent env file.""" nonexistent = pathlib.Path("/nonexistent/.env") - + with pytest.raises(ValueError, match="Environment file not found"): await initialize_pyrit_async(memory_db_type=IN_MEMORY, env_files=[nonexistent]) @@ -219,22 +222,22 @@ async def test_custom_env_files_override_default_behavior(self, mock_set_memory, """Test that passing custom env_files prevents loading default files.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = pathlib.Path(temp_dir) - + # Create default files default_env = temp_path / ".env" default_env_local = temp_path / ".env.local" default_env.write_text("DEFAULT=value") default_env_local.write_text("DEFAULT_LOCAL=value") - + # Create custom file custom_env = temp_path / ".env.custom" custom_env.write_text("CUSTOM=value") - + mock_home_path.__truediv__ = lambda self, other: temp_path / other - + # Pass custom env_files - should NOT load defaults await initialize_pyrit_async(memory_db_type=IN_MEMORY, env_files=[custom_env]) - + # Verify only custom file was loaded, not the default ones assert mock_load_dotenv.call_count == 1 assert mock_load_dotenv.call_args[0][0] == custom_env From 7cabf20c3893e0fba6b3ab81f4262d2038d1aac9 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 12 Dec 2025 13:28:17 -0800 Subject: [PATCH 03/11] pre-commit --- pyrit/auxiliary_attacks/gcg/experiments/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrit/auxiliary_attacks/gcg/experiments/run.py b/pyrit/auxiliary_attacks/gcg/experiments/run.py index 3b21a18f71..bb57d823b4 100644 --- a/pyrit/auxiliary_attacks/gcg/experiments/run.py +++ b/pyrit/auxiliary_attacks/gcg/experiments/run.py @@ -39,7 +39,7 @@ def run_trainer(*, model_name: str, setup: str = "single", **extra_config_parame "Model name not supported. Currently supports 'mistral', 'llama_2', 'llama_3', 'vicuna', and 'phi_3_mini'" ) - _load_environment_files() + _load_environment_files(env_files=None) hf_token = os.environ.get("HUGGINGFACE_TOKEN") if not hf_token: raise ValueError("Please set the HUGGINGFACE_TOKEN environment variable") From d6cc29d99009c291d0265f1365335ed17bfca3ca Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 12 Dec 2025 13:44:59 -0800 Subject: [PATCH 04/11] fixing test --- pyrit/cli/frontend_core.py | 2 +- tests/unit/cli/test_pyrit_shell.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pyrit/cli/frontend_core.py b/pyrit/cli/frontend_core.py index d09ae87b9c..370c53fdfa 100644 --- a/pyrit/cli/frontend_core.py +++ b/pyrit/cli/frontend_core.py @@ -717,7 +717,7 @@ async def print_initializers_list_async(*, context: FrontendCore, discovery_path "initializers": "Built-in initializer names to run before the scenario (e.g., openai_objective_target)", "initialization_scripts": "Paths to custom Python initialization scripts to run before the scenario", "env_files": "Paths to environment files to load in order (e.g., .env.production .env.local). Later files " - "override earlier ones.", + "override earlier ones.", "scenario_strategies": "List of strategy names to run (e.g., base64 rot13)", "max_concurrency": "Maximum number of concurrent attack executions (must be >= 1)", "max_retries": "Maximum number of automatic retries on exception (must be >= 0)", diff --git a/tests/unit/cli/test_pyrit_shell.py b/tests/unit/cli/test_pyrit_shell.py index f984ad0cd1..55f563a271 100644 --- a/tests/unit/cli/test_pyrit_shell.py +++ b/tests/unit/cli/test_pyrit_shell.py @@ -138,6 +138,7 @@ def test_do_run_basic_scenario( "scenario_name": "test_scenario", "initializers": ["test_init"], "initialization_scripts": None, + "env_files": None, "scenario_strategies": None, "max_concurrency": None, "max_retries": None, @@ -197,6 +198,7 @@ def test_do_run_with_initialization_scripts( "scenario_name": "test_scenario", "initializers": None, "initialization_scripts": ["script.py"], + "env_files": None, "scenario_strategies": None, "max_concurrency": None, "max_retries": None, @@ -231,6 +233,7 @@ def test_do_run_with_missing_script( "scenario_name": "test_scenario", "initializers": None, "initialization_scripts": ["missing.py"], + "env_files": None, "scenario_strategies": None, "max_concurrency": None, "max_retries": None, @@ -266,6 +269,7 @@ def test_do_run_with_database_override( "scenario_name": "test_scenario", "initializers": ["test_init"], "initialization_scripts": None, + "env_files": None, "scenario_strategies": None, "max_concurrency": None, "max_retries": None, @@ -306,6 +310,7 @@ def test_do_run_with_exception( "scenario_name": "test_scenario", "initializers": ["test_init"], "initialization_scripts": None, + "env_files": None, "scenario_strategies": None, "max_concurrency": None, "max_retries": None, @@ -683,6 +688,7 @@ def test_run_with_all_parameters( "scenario_name": "test_scenario", "initializers": ["init1"], "initialization_scripts": None, + "env_files": None, "scenario_strategies": ["s1", "s2"], "max_concurrency": 10, "max_retries": 5, @@ -723,6 +729,7 @@ def test_run_stores_result_in_history( "scenario_name": "test_scenario", "initializers": ["test_init"], "initialization_scripts": None, + "env_files": None, "scenario_strategies": None, "max_concurrency": None, "max_retries": None, From 6d1c7a41b87a89065d89f352a8a4f80b2ee57f66 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 12 Dec 2025 13:48:17 -0800 Subject: [PATCH 05/11] another pre-commit --- tests/unit/cli/test_pyrit_shell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/cli/test_pyrit_shell.py b/tests/unit/cli/test_pyrit_shell.py index 55f563a271..ec3e64ffea 100644 --- a/tests/unit/cli/test_pyrit_shell.py +++ b/tests/unit/cli/test_pyrit_shell.py @@ -123,7 +123,6 @@ def test_do_run_empty_line(self, capsys): def test_do_run_basic_scenario( self, mock_parse_args: MagicMock, - mock_run_scenario: AsyncMock, mock_asyncio_run: MagicMock, ): """Test do_run with basic scenario.""" From 67deea80bb585abe911cc4e95c8811b8e56067af Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Fri, 12 Dec 2025 18:50:29 -0800 Subject: [PATCH 06/11] test fix --- tests/unit/cli/test_pyrit_shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/cli/test_pyrit_shell.py b/tests/unit/cli/test_pyrit_shell.py index ec3e64ffea..320d5f5fc2 100644 --- a/tests/unit/cli/test_pyrit_shell.py +++ b/tests/unit/cli/test_pyrit_shell.py @@ -123,6 +123,7 @@ def test_do_run_empty_line(self, capsys): def test_do_run_basic_scenario( self, mock_parse_args: MagicMock, + _mock_run_scenario: AsyncMock, mock_asyncio_run: MagicMock, ): """Test do_run with basic scenario.""" From 682dd2e4dae2b93a6741f1026dbab2a7d84241fb Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Wed, 17 Dec 2025 12:54:34 -0800 Subject: [PATCH 07/11] adding log messages --- pyrit/common/path.py | 2 ++ pyrit/setup/initialization.py | 18 +++++++++++------- tests/unit/setup/test_initialization.py | 14 +++++++------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/pyrit/common/path.py b/pyrit/common/path.py index 40340f28ac..749faf6c19 100644 --- a/pyrit/common/path.py +++ b/pyrit/common/path.py @@ -31,6 +31,8 @@ def in_git_repo() -> bool: PYRIT_PATH = pathlib.Path(__file__, "..", "..").resolve() +CONFIGURATION_DIRECTORY_PATH = pathlib.Path.home() / ".pyrit" + # Points to the root of the project HOME_PATH = pathlib.Path(PYRIT_PATH, "..").resolve() diff --git a/pyrit/setup/initialization.py b/pyrit/setup/initialization.py index 0acaf222c3..a08ebcb005 100644 --- a/pyrit/setup/initialization.py +++ b/pyrit/setup/initialization.py @@ -42,29 +42,33 @@ def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]]) -> None """ # Validate env_files exist if they were provided if env_files is not None: + logger.info(f"Loading custom environment files: {[str(f) for f in env_files]}") for env_file in env_files: if not env_file.exists(): raise ValueError(f"Environment file not found: {env_file}") # By default load .env and .env.local from home directory of the package if env_files is None: + logger.info(f"Checking for default environment files in: {path.CONFIGURATION_DIRECTORY_PATH}") default_files = [] - base_file = path.HOME_PATH / ".env" - local_file = path.HOME_PATH / ".env.local" + base_file = path.CONFIGURATION_DIRECTORY_PATH / ".env" + local_file = path.CONFIGURATION_DIRECTORY_PATH / ".env.local" if base_file.exists(): default_files.append(base_file) if local_file.exists(): default_files.append(local_file) + if default_files: + logger.info(f"Found default environment files: {[str(f) for f in default_files]}") + else: + logger.info("No default environment files found. Using system environment variables only.") + env_files = default_files for env_file in env_files: - if env_file.exists(): - dotenv.load_dotenv(env_file, override=True, interpolate=True) - logger.info(f"Loaded {env_file}") - else: - logger.warning(f"Environment file not found: {env_file}") + dotenv.load_dotenv(env_file, override=True, interpolate=True) + logger.info(f"Loaded environment file: {env_file}") def _load_initializers_from_scripts( diff --git a/tests/unit/setup/test_initialization.py b/tests/unit/setup/test_initialization.py index 3b4d467fd4..bed550b519 100644 --- a/tests/unit/setup/test_initialization.py +++ b/tests/unit/setup/test_initialization.py @@ -115,8 +115,8 @@ class TestLoadEnvironmentFiles: @pytest.mark.asyncio @mock.patch("pyrit.setup.initialization.dotenv.load_dotenv") - @mock.patch("pyrit.setup.initialization.path.HOME_PATH") - async def test_loads_default_env_files_when_none_provided(self, mock_home_path, mock_load_dotenv): + @mock.patch("pyrit.setup.initialization.path.CONFIGURATION_DIRECTORY_PATH") + async def test_loads_default_env_files_when_none_provided(self, mock_config_path, mock_load_dotenv): """Test that default .env and .env.local files are loaded when env_files is None.""" # Create temporary directory and files with tempfile.TemporaryDirectory() as temp_dir: @@ -128,8 +128,8 @@ async def test_loads_default_env_files_when_none_provided(self, mock_home_path, env_file.write_text("VAR1=value1") env_local_file.write_text("VAR2=value2") - # Mock HOME_PATH to point to our temp directory - mock_home_path.__truediv__ = lambda self, other: temp_path / other + # Mock CONFIGURATION_DIRECTORY_PATH to point to our temp directory + mock_config_path.__truediv__ = lambda self, other: temp_path / other # Call the function with None (default behavior) _load_environment_files(env_files=None) @@ -142,8 +142,8 @@ async def test_loads_default_env_files_when_none_provided(self, mock_home_path, @pytest.mark.asyncio @mock.patch("pyrit.setup.initialization.dotenv.load_dotenv") - @mock.patch("pyrit.setup.initialization.path.HOME_PATH") - async def test_only_loads_existing_default_files(self, mock_home_path, mock_load_dotenv): + @mock.patch("pyrit.setup.initialization.path.CONFIGURATION_DIRECTORY_PATH") + async def test_only_loads_existing_default_files(self, mock_config_path, mock_load_dotenv): """Test that only existing default files are loaded.""" with tempfile.TemporaryDirectory() as temp_dir: temp_path = pathlib.Path(temp_dir) @@ -152,7 +152,7 @@ async def test_only_loads_existing_default_files(self, mock_home_path, mock_load # Only create .env, not .env.local env_file.write_text("VAR1=value1") - mock_home_path.__truediv__ = lambda self, other: temp_path / other + mock_config_path.__truediv__ = lambda self, other: temp_path / other _load_environment_files(env_files=None) From bc6a4f3b95e554158efac295326f7fd14a2209b7 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Thu, 18 Dec 2025 12:37:40 -0800 Subject: [PATCH 08/11] adding print messages for clarity --- pyrit/setup/initialization.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pyrit/setup/initialization.py b/pyrit/setup/initialization.py index a08ebcb005..6c6992528c 100644 --- a/pyrit/setup/initialization.py +++ b/pyrit/setup/initialization.py @@ -28,7 +28,7 @@ MemoryDatabaseType = Literal["InMemory", "SQLite", "AzureSQL"] -def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]]) -> None: +def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]], *, silent: bool = False) -> None: """ Load environment files in the order they are provided. Later files override values from earlier files. @@ -36,20 +36,22 @@ def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]]) -> None Args: env_files: Optional sequence of environment file paths. If None, loads default .env and .env.local from PyRIT home directory (only if they exist). + silent: If True, suppresses print statements about environment file loading. + Defaults to False. Raises: ValueError: If any provided env_files do not exist. """ # Validate env_files exist if they were provided if env_files is not None: - logger.info(f"Loading custom environment files: {[str(f) for f in env_files]}") + if not silent: + print(f"Loading custom environment files: {[str(f) for f in env_files]}") for env_file in env_files: if not env_file.exists(): raise ValueError(f"Environment file not found: {env_file}") # By default load .env and .env.local from home directory of the package if env_files is None: - logger.info(f"Checking for default environment files in: {path.CONFIGURATION_DIRECTORY_PATH}") default_files = [] base_file = path.CONFIGURATION_DIRECTORY_PATH / ".env" local_file = path.CONFIGURATION_DIRECTORY_PATH / ".env.local" @@ -59,16 +61,19 @@ def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]]) -> None if local_file.exists(): default_files.append(local_file) - if default_files: - logger.info(f"Found default environment files: {[str(f) for f in default_files]}") - else: - logger.info("No default environment files found. Using system environment variables only.") + if not silent: + if default_files: + print(f"Found default environment files: {[str(f) for f in default_files]}") + else: + print("No default environment files found. Using system environment variables only.") env_files = default_files for env_file in env_files: dotenv.load_dotenv(env_file, override=True, interpolate=True) logger.info(f"Loaded environment file: {env_file}") + if not silent: + print(f"Loaded environment file: {env_file}") def _load_initializers_from_scripts( @@ -210,6 +215,7 @@ async def initialize_pyrit_async( initialization_scripts: Optional[Sequence[Union[str, pathlib.Path]]] = None, initializers: Optional[Sequence["PyRITInitializer"]] = None, env_files: Optional[Sequence[pathlib.Path]] = None, + silent: bool = False, **memory_instance_kwargs: Any, ) -> None: """ @@ -226,12 +232,14 @@ async def initialize_pyrit_async( env_files (Optional[Sequence[pathlib.Path]]): Optional sequence of environment file paths to load in order. If not provided, will load default .env and .env.local files from PyRIT home if they exist. All paths must be valid pathlib.Path objects. + silent (bool): If True, suppresses print statements about environment file loading. + Defaults to False. **memory_instance_kwargs (Optional[Any]): Additional keyword arguments to pass to the memory instance. Raises: ValueError: If an unsupported memory_db_type is provided or if env_files contains non-existent files. """ - _load_environment_files(env_files=env_files) + _load_environment_files(env_files=env_files, silent=silent) # Reset all default values before executing initialization scripts # This ensures a clean state for each initialization From 02f038331ff6dc4d138a57513e1092a25b1b7770 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Thu, 18 Dec 2025 12:55:28 -0800 Subject: [PATCH 09/11] making print a function --- pyrit/setup/initialization.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/pyrit/setup/initialization.py b/pyrit/setup/initialization.py index 6c6992528c..b8ebfcf453 100644 --- a/pyrit/setup/initialization.py +++ b/pyrit/setup/initialization.py @@ -45,13 +45,13 @@ def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]], *, sile # Validate env_files exist if they were provided if env_files is not None: if not silent: - print(f"Loading custom environment files: {[str(f) for f in env_files]}") + _print_msg(f"Loading custom environment files: {[str(f) for f in env_files]}", quiet=silent, log=True) for env_file in env_files: if not env_file.exists(): raise ValueError(f"Environment file not found: {env_file}") # By default load .env and .env.local from home directory of the package - if env_files is None: + else: default_files = [] base_file = path.CONFIGURATION_DIRECTORY_PATH / ".env" local_file = path.CONFIGURATION_DIRECTORY_PATH / ".env.local" @@ -63,17 +63,37 @@ def _load_environment_files(env_files: Optional[Sequence[pathlib.Path]], *, sile if not silent: if default_files: - print(f"Found default environment files: {[str(f) for f in default_files]}") + _print_msg( + f"Found default environment files: {[str(f) for f in default_files]}", quiet=silent, log=True + ) else: - print("No default environment files found. Using system environment variables only.") + _print_msg( + "No default environment files found. Using system environment variables only.", + quiet=silent, + log=True, + ) env_files = default_files for env_file in env_files: dotenv.load_dotenv(env_file, override=True, interpolate=True) - logger.info(f"Loaded environment file: {env_file}") if not silent: - print(f"Loaded environment file: {env_file}") + _print_msg(f"Loaded environment file: {env_file}", quiet=silent, log=True) + + +def _print_msg(message: str, quiet: bool, log: bool) -> None: + """ + Print a standard initialization message unless quiet is True. + + Args: + message (str): The message to print and/or log. + quiet (bool): If True, suppresses the initialization message. + log (bool): If True, logs the message using the logger. + """ + if not quiet: + print(message) + if log: + logger.info(message) def _load_initializers_from_scripts( From 0489af0153c6e80943a42123c8fc9ae83a9b01bd Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Mon, 22 Dec 2025 16:21:46 -0800 Subject: [PATCH 10/11] Updating docs --- .env_example | 2 +- doc/setup/populating_secrets.md | 55 ++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.env_example b/.env_example index 2016926fd9..656cfeee64 100644 --- a/.env_example +++ b/.env_example @@ -1,4 +1,4 @@ -# This is an example of the .env file. Copy to .env and fill in your secrets. +# This is an example of the .env file. Copy to ~/.pyrit/.env and fill in your endpoint configurations. # Note that if you are using Entra authentication for certain Azure resources (use_entra_auth = True in PyRIT), # keys for those resources are not needed. diff --git a/doc/setup/populating_secrets.md b/doc/setup/populating_secrets.md index 7c2506d235..202f5c9be9 100644 --- a/doc/setup/populating_secrets.md +++ b/doc/setup/populating_secrets.md @@ -21,11 +21,37 @@ With this setup, you can run most PyRIT notebooks and examples! ## Setting Up Environment Variables -PyRIT loads secrets and endpoints from environment variables or a `.env` file in your repo root. The `.env_example` file shows the format and available options. +PyRIT loads secrets and endpoints from environment variables or `.env` files. The `.env_example` file shows the format and available options. + +### Environment Variable Precedence + +When `initialize_pyrit_async` runs, environment variables are loaded in a specific order. **Later sources override earlier ones:** + +```mermaid +flowchart LR + A["1️⃣ System Environment"] --> B{"env_files provided?"} + B -->|No| C["2️⃣ ~/.pyrit/.env"] + C --> D["3️⃣ ~/.pyrit/.env.local"] + B -->|Yes| E["2️⃣ Your specified files
(in order)"] + style A fill:#e8e8e8 + style C fill:#b3d9ff + style D fill:#90EE90 + style E fill:#FFD700 +``` + +**Default behavior** (no `env_files` argument): + +| Priority | Source | Description | +|----------|--------|-------------| +| Lowest | System environment variables | Always loaded as the baseline | +| Medium | `~/.pyrit/.env` | Default config file (loaded if it exists) | +| Highest | `~/.pyrit/.env.local` | Local overrides (loaded if it exists) | + +**Custom behavior** (with `env_files` argument): Only your specified files are loaded, in order. Default paths are completely ignored. ### Creating Your .env File -1. Copy `.env_example` to `.env` in your repository root +1. Copy `.env_example` to `.env` in your home directory in ~/.pyrit/.env 2. Add your API credentials. For example, for Azure OpenAI: ```bash @@ -37,12 +63,33 @@ To find these values in Azure Portal: `Azure Portal > Azure AI Services > Azure ### Using .env.local for Overrides -You can use `.env.local` to override values in `.env` without modifying the base file. This is useful for: +You can use `~/.pyrit/.env.local` to override values in `~/.pyrit/.env` without modifying the base file. This is useful for: - Testing different targets - Using personal credentials instead of shared ones - Switching between configurations quickly -Simply create `.env.local` and add any variables you want to override. PyRIT will prioritize `.env.local` over `.env`. +Simply create `.env.local` in your `~/.pyrit/` directory and add any variables you want to override. + +### Custom Environment Files + +You can also specify exactly which `.env` files to load using the `env_files` parameter: + +```python +from pathlib import Path +from pyrit.setup import initialize_pyrit_async + +await initialize_pyrit_async( + memory_db_type="InMemory", + env_files=[Path("./project-config.env"), Path("./local-overrides.env")] +) +``` + +When `env_files` is provided: +- **Only** the specified files are loaded (default paths are skipped entirely) +- Files are loaded in order—later files override earlier ones +- A `ValueError` is raised if any specified file doesn't exist + +The CLI also supports custom environment files via the `--env-files` flag. ## Authentication Options From 84db0a3b07e6c80a99fe5135f1e0f7fe5a5883a5 Mon Sep 17 00:00:00 2001 From: Richard Lundeen Date: Mon, 22 Dec 2025 19:15:08 -0800 Subject: [PATCH 11/11] fixing mermaid diagram --- doc/setup/populating_secrets.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/doc/setup/populating_secrets.md b/doc/setup/populating_secrets.md index 202f5c9be9..3861aa9c14 100644 --- a/doc/setup/populating_secrets.md +++ b/doc/setup/populating_secrets.md @@ -27,16 +27,12 @@ PyRIT loads secrets and endpoints from environment variables or `.env` files. Th When `initialize_pyrit_async` runs, environment variables are loaded in a specific order. **Later sources override earlier ones:** -```mermaid +```{mermaid} flowchart LR - A["1️⃣ System Environment"] --> B{"env_files provided?"} - B -->|No| C["2️⃣ ~/.pyrit/.env"] - C --> D["3️⃣ ~/.pyrit/.env.local"] - B -->|Yes| E["2️⃣ Your specified files
(in order)"] - style A fill:#e8e8e8 - style C fill:#b3d9ff - style D fill:#90EE90 - style E fill:#FFD700 + A["1. System Environment"] --> B{"env_files provided?"} + B -->|No| C["2. ~/.pyrit/.env"] + C --> D["3. ~/.pyrit/.env.local"] + B -->|Yes| E["2. Your specified files (in order)"] ``` **Default behavior** (no `env_files` argument):