vaulttool 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vaulttool/__init__.py +91 -0
- vaulttool/__main__.py +5 -0
- vaulttool/cli.py +365 -0
- vaulttool/config.py +264 -0
- vaulttool/core.py +895 -0
- vaulttool/tests/test_checksum_trigger.py +48 -0
- vaulttool/tests/test_config.py +25 -0
- vaulttool/tests/test_core.py +672 -0
- vaulttool/tests/test_decrypt_missing_sources.py +47 -0
- vaulttool/tests/test_env_config.py +294 -0
- vaulttool/tests/test_error_aggregation.py +374 -0
- vaulttool/tests/test_gitignore.py +47 -0
- vaulttool/tests/test_key_file_validation.py +139 -0
- vaulttool/tests/test_main_integration.py +60 -0
- vaulttool/tests/test_priority3_fixes.py +360 -0
- vaulttool/tests/test_security_and_edge_cases.py +483 -0
- vaulttool/tests/test_suffix_fallback.py +390 -0
- vaulttool/tests/test_utils.py +79 -0
- vaulttool/utils.py +251 -0
- vaulttool-2.0.0.dist-info/LICENSE +202 -0
- vaulttool-2.0.0.dist-info/METADATA +880 -0
- vaulttool-2.0.0.dist-info/RECORD +24 -0
- vaulttool-2.0.0.dist-info/WHEEL +4 -0
- vaulttool-2.0.0.dist-info/entry_points.txt +3 -0
vaulttool/config.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Configuration loading and validation for VaultTool.
|
|
2
|
+
|
|
3
|
+
This module handles loading VaultTool configuration from YAML files,
|
|
4
|
+
with support for multiple configuration file locations, environment variable
|
|
5
|
+
overrides, and validation of required configuration keys.
|
|
6
|
+
|
|
7
|
+
Environment Variable Support:
|
|
8
|
+
Configuration can be overridden using environment variables with the
|
|
9
|
+
VAULTTOOL_ prefix. For list values, use comma-separated strings.
|
|
10
|
+
|
|
11
|
+
Examples:
|
|
12
|
+
VAULTTOOL_OPTIONS_KEY_FILE=/path/to/key
|
|
13
|
+
VAULTTOOL_OPTIONS_USE_SUFFIX_FALLBACK=true
|
|
14
|
+
VAULTTOOL_INCLUDE_PATTERNS="*.env,*.secret"
|
|
15
|
+
VAULTTOOL_EXCLUDE_DIRECTORIES=".git,.venv"
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_bool(value: str) -> bool:
|
|
26
|
+
"""Parse boolean value from string.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
value: String value to parse (case-insensitive)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Boolean value
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> _parse_bool("true")
|
|
36
|
+
True
|
|
37
|
+
>>> _parse_bool("false")
|
|
38
|
+
False
|
|
39
|
+
>>> _parse_bool("1")
|
|
40
|
+
True
|
|
41
|
+
"""
|
|
42
|
+
return value.lower() in ("true", "yes", "1", "on")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_list(value: str) -> List[str]:
|
|
46
|
+
"""Parse comma-separated list from string.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
value: Comma-separated string
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of stripped strings
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> _parse_list("*.env, *.ini, *.secret")
|
|
56
|
+
['*.env', '*.ini', '*.secret']
|
|
57
|
+
"""
|
|
58
|
+
if not value:
|
|
59
|
+
return []
|
|
60
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _apply_env_overrides(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
64
|
+
"""Apply environment variable overrides to configuration.
|
|
65
|
+
|
|
66
|
+
Environment variables with VAULTTOOL_ prefix override config values.
|
|
67
|
+
Nested keys use double underscores, e.g., VAULTTOOL_OPTIONS_KEY_FILE.
|
|
68
|
+
Lists use comma-separated values.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
config: Base configuration dictionary
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Configuration with environment overrides applied
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
export VAULTTOOL_OPTIONS_KEY_FILE=/custom/key
|
|
78
|
+
export VAULTTOOL_INCLUDE_PATTERNS="*.env,*.secret"
|
|
79
|
+
"""
|
|
80
|
+
env_prefix = "VAULTTOOL_"
|
|
81
|
+
|
|
82
|
+
# Map of config keys to environment variable names
|
|
83
|
+
env_mappings = {
|
|
84
|
+
# Top-level lists
|
|
85
|
+
"include_directories": f"{env_prefix}INCLUDE_DIRECTORIES",
|
|
86
|
+
"exclude_directories": f"{env_prefix}EXCLUDE_DIRECTORIES",
|
|
87
|
+
"include_patterns": f"{env_prefix}INCLUDE_PATTERNS",
|
|
88
|
+
"exclude_patterns": f"{env_prefix}EXCLUDE_PATTERNS",
|
|
89
|
+
|
|
90
|
+
# Options (nested)
|
|
91
|
+
"options.suffix": f"{env_prefix}OPTIONS_SUFFIX",
|
|
92
|
+
"options.key_file": f"{env_prefix}OPTIONS_KEY_FILE",
|
|
93
|
+
"options.algorithm": f"{env_prefix}OPTIONS_ALGORITHM",
|
|
94
|
+
"options.openssl_path": f"{env_prefix}OPTIONS_OPENSSL_PATH",
|
|
95
|
+
"options.use_suffix_fallback": f"{env_prefix}OPTIONS_USE_SUFFIX_FALLBACK",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for config_key, env_var in env_mappings.items():
|
|
99
|
+
env_value = os.environ.get(env_var)
|
|
100
|
+
if env_value is None:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
# Parse the configuration key path
|
|
104
|
+
if "." in config_key:
|
|
105
|
+
# Nested key (e.g., "options.key_file")
|
|
106
|
+
parent_key, child_key = config_key.split(".", 1)
|
|
107
|
+
|
|
108
|
+
# Ensure parent exists
|
|
109
|
+
if parent_key not in config:
|
|
110
|
+
config[parent_key] = {}
|
|
111
|
+
if not isinstance(config[parent_key], dict):
|
|
112
|
+
config[parent_key] = {}
|
|
113
|
+
|
|
114
|
+
# Determine value type and parse
|
|
115
|
+
if child_key in ("use_suffix_fallback",):
|
|
116
|
+
# Boolean values
|
|
117
|
+
config[parent_key][child_key] = _parse_bool(env_value)
|
|
118
|
+
else:
|
|
119
|
+
# String values
|
|
120
|
+
config[parent_key][child_key] = env_value
|
|
121
|
+
else:
|
|
122
|
+
# Top-level key
|
|
123
|
+
if config_key.endswith(("_directories", "_patterns")):
|
|
124
|
+
# List values
|
|
125
|
+
config[config_key] = _parse_list(env_value)
|
|
126
|
+
else:
|
|
127
|
+
# String values
|
|
128
|
+
config[config_key] = env_value
|
|
129
|
+
|
|
130
|
+
return config
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def load_config(path: str = ".vaulttool.yml") -> Dict[str, Any]:
|
|
134
|
+
"""Load and validate the VaultTool configuration file.
|
|
135
|
+
|
|
136
|
+
Attempts to load configuration from the specified path. If the file doesn't
|
|
137
|
+
exist, searches for configuration files in standard locations:
|
|
138
|
+
|
|
139
|
+
1. Current directory: .vaulttool.yml
|
|
140
|
+
2. User config: ~/.vaulttool/.vaulttool.yml
|
|
141
|
+
3. System config: /etc/vaulttool/config.yml
|
|
142
|
+
|
|
143
|
+
After loading, applies environment variable overrides with VAULTTOOL_ prefix.
|
|
144
|
+
Environment variables override file configuration.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
path: Path to the configuration file. Defaults to ".vaulttool.yml".
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dictionary containing the parsed and validated configuration.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
FileNotFoundError: If no configuration file is found in any location.
|
|
154
|
+
ValueError: If the configuration file is invalid, malformed, or
|
|
155
|
+
missing required keys.
|
|
156
|
+
yaml.YAMLError: If the YAML file cannot be parsed.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> config = load_config()
|
|
160
|
+
>>> print(config['options']['algorithm'])
|
|
161
|
+
'aes-256-cbc'
|
|
162
|
+
|
|
163
|
+
>>> # With environment override:
|
|
164
|
+
>>> os.environ['VAULTTOOL_OPTIONS_KEY_FILE'] = '/custom/key'
|
|
165
|
+
>>> config = load_config()
|
|
166
|
+
>>> print(config['options']['key_file'])
|
|
167
|
+
'/custom/key'
|
|
168
|
+
"""
|
|
169
|
+
config_path = Path(path)
|
|
170
|
+
|
|
171
|
+
if not config_path.exists():
|
|
172
|
+
# search for ~/.vaulttool/.vaulttool.yml
|
|
173
|
+
user_config_path = Path.home() / ".vaulttool" / ".vaulttool.yml"
|
|
174
|
+
if user_config_path.exists():
|
|
175
|
+
config_path = user_config_path
|
|
176
|
+
else:
|
|
177
|
+
etc_config_path = Path("/etc/vaulttool/config.yml")
|
|
178
|
+
if etc_config_path.exists():
|
|
179
|
+
config_path = etc_config_path
|
|
180
|
+
|
|
181
|
+
if not config_path.exists():
|
|
182
|
+
raise FileNotFoundError(
|
|
183
|
+
f"Configuration file not found at {path}. Please create a .vaulttool.yml file or specify the correct path."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
187
|
+
data = yaml.safe_load(f) or {}
|
|
188
|
+
|
|
189
|
+
# Support the documented structure with a top-level `vaulttool` key
|
|
190
|
+
cfg = data.get("vaulttool", data)
|
|
191
|
+
|
|
192
|
+
# Ensure the config has the required keys
|
|
193
|
+
if cfg is None or not isinstance(cfg, dict):
|
|
194
|
+
raise ValueError("Invalid configuration format. Expected a dictionary.")
|
|
195
|
+
|
|
196
|
+
# Apply environment variable overrides
|
|
197
|
+
cfg = _apply_env_overrides(cfg)
|
|
198
|
+
|
|
199
|
+
required_keys = [
|
|
200
|
+
"include_directories",
|
|
201
|
+
"exclude_directories",
|
|
202
|
+
"include_patterns",
|
|
203
|
+
"exclude_patterns",
|
|
204
|
+
"options",
|
|
205
|
+
]
|
|
206
|
+
missing = [k for k in required_keys if k not in cfg]
|
|
207
|
+
if missing:
|
|
208
|
+
raise ValueError(f"Missing required configuration keys: {', '.join(missing)}")
|
|
209
|
+
|
|
210
|
+
# Validate types of required keys
|
|
211
|
+
if not isinstance(cfg.get("include_directories"), list):
|
|
212
|
+
raise ValueError("'include_directories' must be a list")
|
|
213
|
+
if not isinstance(cfg.get("exclude_directories"), list):
|
|
214
|
+
raise ValueError("'exclude_directories' must be a list")
|
|
215
|
+
if not isinstance(cfg.get("include_patterns"), list):
|
|
216
|
+
raise ValueError("'include_patterns' must be a list")
|
|
217
|
+
if not isinstance(cfg.get("exclude_patterns"), list):
|
|
218
|
+
raise ValueError("'exclude_patterns' must be a list")
|
|
219
|
+
|
|
220
|
+
# Validate include_patterns is not empty
|
|
221
|
+
if not cfg.get("include_patterns"):
|
|
222
|
+
raise ValueError("'include_patterns' cannot be empty - at least one pattern required")
|
|
223
|
+
|
|
224
|
+
# Basic validation of options block
|
|
225
|
+
options = cfg.get("options")
|
|
226
|
+
if not isinstance(options, dict):
|
|
227
|
+
raise ValueError("'options' must be a mapping/dictionary.")
|
|
228
|
+
|
|
229
|
+
# Validate key_file is specified
|
|
230
|
+
if "key_file" not in options:
|
|
231
|
+
raise ValueError("'options.key_file' is required")
|
|
232
|
+
if not isinstance(options.get("key_file"), str):
|
|
233
|
+
raise ValueError("'options.key_file' must be a string")
|
|
234
|
+
if not options.get("key_file"):
|
|
235
|
+
raise ValueError("'options.key_file' cannot be empty")
|
|
236
|
+
|
|
237
|
+
# Suffix validation logic
|
|
238
|
+
suffix = options.get("suffix")
|
|
239
|
+
if isinstance(suffix, str):
|
|
240
|
+
# Check if suffix contains a dot at all
|
|
241
|
+
if "." not in suffix:
|
|
242
|
+
raise ValueError("Suffix must contain a dot (e.g., .vault, .secret.vault)")
|
|
243
|
+
|
|
244
|
+
# If suffix comes from environment variable, enforce .vault ending
|
|
245
|
+
# Check if VAULTTOOL_OPTIONS_SUFFIX is set
|
|
246
|
+
if os.environ.get("VAULTTOOL_OPTIONS_SUFFIX"):
|
|
247
|
+
if not suffix.endswith(".vault"):
|
|
248
|
+
raise ValueError(f"'options.suffix' from environment must end with '.vault', got: {suffix}")
|
|
249
|
+
|
|
250
|
+
if not suffix.startswith("."):
|
|
251
|
+
# If a dot is present but doesn't start with dot, ensure it starts with underscore
|
|
252
|
+
if not suffix.startswith("_"):
|
|
253
|
+
# Prepend underscore
|
|
254
|
+
new_suffix = f"_{suffix}"
|
|
255
|
+
options["suffix"] = new_suffix
|
|
256
|
+
|
|
257
|
+
# Ensure exclude_patterns contains the suffix pattern
|
|
258
|
+
exclude_patterns = cfg.get("exclude_patterns", [])
|
|
259
|
+
suffix_pattern = f"*{suffix}" if suffix else "*.vault"
|
|
260
|
+
if suffix_pattern not in exclude_patterns:
|
|
261
|
+
# Add suffix pattern to exclude_patterns to avoid self-encryption
|
|
262
|
+
cfg["exclude_patterns"].append(suffix_pattern)
|
|
263
|
+
|
|
264
|
+
return cfg
|