vaulttool 2.0.1__tar.gz → 2.0.2__tar.gz
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-2.0.1 → vaulttool-2.0.2}/PKG-INFO +5 -4
- {vaulttool-2.0.1 → vaulttool-2.0.2}/pyproject.toml +4 -4
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/core.py +82 -3
- vaulttool-2.0.2/vaulttool/tests/test_suffix_switch_bug.py +142 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/LICENSE +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/README.md +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/__init__.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/__main__.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/cli.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/config.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/conftest.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_checksum_trigger.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_config.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_core.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_decrypt_missing_sources.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_env_config.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_error_aggregation.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_gitignore.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_key_file_validation.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_main_integration.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_priority3_fixes.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_security_and_edge_cases.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_suffix_fallback.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_utils.py +0 -0
- {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/utils.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: vaulttool
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Secure file encryption for secrets/configs
|
|
5
|
-
Home-page: https://github.com/yourusername/vaulttool
|
|
6
5
|
License: Apache-2
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Author: Josef Fuchs
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Classifier: License :: Other/Proprietary License
|
|
@@ -12,10 +12,11 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
16
|
Requires-Dist: cryptography
|
|
16
|
-
Requires-Dist: pytest (>=8.4.2,<9.0.0)
|
|
17
17
|
Requires-Dist: pyyaml
|
|
18
18
|
Requires-Dist: typer
|
|
19
|
+
Project-URL: Homepage, https://github.com/yourusername/vaulttool
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
|
|
21
22
|
# Vault Tool
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "vaulttool"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.2"
|
|
4
4
|
description = "Secure file encryption for secrets/configs"
|
|
5
|
-
authors = ["Josef Fuchs"]
|
|
6
|
-
license = {
|
|
5
|
+
authors = [{name = "Josef Fuchs"}]
|
|
6
|
+
license = {text = "Apache-2"}
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
dependencies = [
|
|
@@ -18,7 +18,7 @@ vaulttool = "vaulttool.cli:app"
|
|
|
18
18
|
|
|
19
19
|
[tool.poetry]
|
|
20
20
|
name = "vaulttool"
|
|
21
|
-
version = "2.0.
|
|
21
|
+
version = "2.0.2"
|
|
22
22
|
description = "Secure file encryption for secrets/configs"
|
|
23
23
|
authors = ["Josef Fuchs"]
|
|
24
24
|
license = "Apache-2"
|
|
@@ -218,6 +218,73 @@ class VaultTool:
|
|
|
218
218
|
# Simple suffix appending
|
|
219
219
|
return f"{source_path}{suffix}"
|
|
220
220
|
|
|
221
|
+
def infer_vault_suffix(self, vault_path: str) -> str | None:
|
|
222
|
+
"""Infer the actual vault suffix from a vault file path.
|
|
223
|
+
|
|
224
|
+
When fallback is enabled, vault files might have different suffixes than the current
|
|
225
|
+
configured suffix. This method attempts to determine the actual suffix by checking
|
|
226
|
+
if the file ends with ".vault" but not with the current suffix.
|
|
227
|
+
|
|
228
|
+
Strategy: Since all suffixes must end with ".vault", when a file doesn't match the
|
|
229
|
+
current suffix but ends with ".vault", we infer the suffix while accounting for
|
|
230
|
+
common file extensions (.env, .ini, .yaml, etc.).
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
vault_path: Path to the vault file to analyze.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
The inferred suffix (e.g., ".prod.vault", "_n3test.vault") or None if unable to infer.
|
|
237
|
+
|
|
238
|
+
Example:
|
|
239
|
+
>>> vt.infer_vault_suffix("development/creds.env.prod.vault")
|
|
240
|
+
".prod.vault"
|
|
241
|
+
"""
|
|
242
|
+
if not isinstance(vault_path, str):
|
|
243
|
+
vault_path = str(vault_path)
|
|
244
|
+
|
|
245
|
+
# If the file already matches the current suffix, return it
|
|
246
|
+
if vault_path.endswith(self.suffix):
|
|
247
|
+
return self.suffix
|
|
248
|
+
|
|
249
|
+
# If fallback is enabled and file ends with .vault, we need to infer the suffix
|
|
250
|
+
if self.use_suffix_fallback and vault_path.endswith(".vault"):
|
|
251
|
+
# Remove ".vault" to get the base filename
|
|
252
|
+
base = vault_path[:-6] # Remove ".vault"
|
|
253
|
+
|
|
254
|
+
# Common file extensions that are NOT part of the vault suffix
|
|
255
|
+
# These are file extensions that come BEFORE the vault suffix
|
|
256
|
+
COMMON_EXTENSIONS = {
|
|
257
|
+
'.env', '.ini', '.yaml', '.yml', '.json', '.conf', '.cfg',
|
|
258
|
+
'.config', '.secret', '.txt', '.log', '.properties', '.cnf'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# Check if the base ends with a common file extension
|
|
262
|
+
for ext in COMMON_EXTENSIONS:
|
|
263
|
+
if base.endswith(ext):
|
|
264
|
+
# The file extension is part of the filename, not the vault suffix
|
|
265
|
+
# So the vault suffix is just ".vault"
|
|
266
|
+
logger.debug(f"Detected common file extension {ext!r} in {vault_path!r}, using .vault as suffix")
|
|
267
|
+
return ".vault"
|
|
268
|
+
|
|
269
|
+
# No common extension found, so try to find a custom suffix
|
|
270
|
+
# Look for the last dot or underscore that could mark the start of a custom suffix
|
|
271
|
+
last_dot_idx = base.rfind(".")
|
|
272
|
+
last_underscore_idx = base.rfind("_")
|
|
273
|
+
|
|
274
|
+
# Take the last occurrence of either dot or underscore
|
|
275
|
+
split_idx = max(last_dot_idx, last_underscore_idx)
|
|
276
|
+
|
|
277
|
+
if split_idx >= 0:
|
|
278
|
+
# Found a dot or underscore, so custom suffix is from that position to ".vault"
|
|
279
|
+
inferred_suffix = base[split_idx:] + ".vault"
|
|
280
|
+
logger.debug(f"Inferred vault suffix {inferred_suffix!r} for file {vault_path!r}")
|
|
281
|
+
return inferred_suffix
|
|
282
|
+
else:
|
|
283
|
+
# No dot or underscore found in base, so suffix is just ".vault"
|
|
284
|
+
return ".vault"
|
|
285
|
+
|
|
286
|
+
return None
|
|
287
|
+
|
|
221
288
|
def encrypt_file(self, source_path: str, encrypted_path: str):
|
|
222
289
|
"""Encrypt a single file using AES-256-CBC with derived encryption key.
|
|
223
290
|
|
|
@@ -453,8 +520,14 @@ class VaultTool:
|
|
|
453
520
|
if str(vault_file).endswith(self.suffix):
|
|
454
521
|
continue
|
|
455
522
|
|
|
456
|
-
|
|
457
|
-
|
|
523
|
+
# Infer the actual suffix instead of assuming ".vault"
|
|
524
|
+
inferred_suffix = self.infer_vault_suffix(str(vault_file))
|
|
525
|
+
if inferred_suffix is None:
|
|
526
|
+
# Fallback to ".vault" if unable to infer
|
|
527
|
+
inferred_suffix = ".vault"
|
|
528
|
+
|
|
529
|
+
source = self.source_filename(str(vault_file), inferred_suffix)
|
|
530
|
+
# Only use fallback if custom suffix doesn't exist
|
|
458
531
|
if source not in vault_files_by_source:
|
|
459
532
|
vault_files_by_source[source] = vault_file
|
|
460
533
|
|
|
@@ -535,7 +608,13 @@ class VaultTool:
|
|
|
535
608
|
if str(vault_file).endswith(self.suffix):
|
|
536
609
|
vault_suffix = self.suffix
|
|
537
610
|
elif self.use_suffix_fallback and str(vault_file).endswith(".vault"):
|
|
538
|
-
|
|
611
|
+
# Use inferred suffix instead of assuming ".vault"
|
|
612
|
+
vault_suffix = self.infer_vault_suffix(str(vault_file))
|
|
613
|
+
if vault_suffix is None:
|
|
614
|
+
logger.warning(f"Failed to infer vault suffix for {vault_file}")
|
|
615
|
+
skipped += 1
|
|
616
|
+
continue
|
|
617
|
+
logger.debug(f"Using inferred suffix {vault_suffix!r} for {vault_file}")
|
|
539
618
|
else:
|
|
540
619
|
# Unknown suffix, skip
|
|
541
620
|
logger.warning(f"Vault file {vault_file} doesn't match any known suffix pattern")
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Test for bug fix: refresh creates wrong filename when suffix changes.
|
|
2
|
+
|
|
3
|
+
This test suite addresses the issue where `vaulttool refresh` would create
|
|
4
|
+
a file with an incorrect name (missing source filename) when the suffix
|
|
5
|
+
configuration changed between encryption and refresh operations.
|
|
6
|
+
|
|
7
|
+
Issue:
|
|
8
|
+
- Encrypt file with VAULTTOOL_OPTIONS_SUFFIX=n3test.vault creates development/creds.env_n3test.vault
|
|
9
|
+
- Run refresh → should restore development/creds.env
|
|
10
|
+
- BUG: created development/creds.env_n3test instead
|
|
11
|
+
|
|
12
|
+
Root Cause:
|
|
13
|
+
When a vault file from a previous suffix (e.g., .prod.vault) is found during refresh
|
|
14
|
+
with a different current suffix (e.g., _n3test.vault), the fallback logic would
|
|
15
|
+
incorrectly use ".vault" as the suffix, removing only 6 characters instead of 12,
|
|
16
|
+
resulting in an incorrect source filename.
|
|
17
|
+
|
|
18
|
+
Fix:
|
|
19
|
+
infer_vault_suffix() method now intelligently determines the actual vault suffix
|
|
20
|
+
by recognizing common file extensions (.env, .ini, etc.) and correctly inferring
|
|
21
|
+
custom suffixes like .prod.vault.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import pytest
|
|
26
|
+
import tempfile
|
|
27
|
+
import shutil
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from vaulttool.core import VaultTool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestSuffixSwitchBugFix:
|
|
33
|
+
"""Test that refresh correctly handles vault files with different suffix than current."""
|
|
34
|
+
|
|
35
|
+
def setup_method(self):
|
|
36
|
+
"""Set up test environment."""
|
|
37
|
+
try:
|
|
38
|
+
self.original_cwd = os.getcwd()
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
self.original_cwd = Path.home()
|
|
41
|
+
|
|
42
|
+
self.test_dir = tempfile.mkdtemp()
|
|
43
|
+
os.chdir(self.test_dir)
|
|
44
|
+
|
|
45
|
+
# Create key file
|
|
46
|
+
self.key_file = Path('test.key')
|
|
47
|
+
self.key_file.write_text('a' * 32)
|
|
48
|
+
|
|
49
|
+
# Create development directory
|
|
50
|
+
Path('development').mkdir(exist_ok=True)
|
|
51
|
+
|
|
52
|
+
def teardown_method(self):
|
|
53
|
+
"""Clean up test environment."""
|
|
54
|
+
os.chdir(self.original_cwd)
|
|
55
|
+
shutil.rmtree(self.test_dir)
|
|
56
|
+
|
|
57
|
+
def test_env_variable_suffix_refresh_restores_correct_file(self):
|
|
58
|
+
"""Test the exact bug scenario: suffix changed via env var, refresh restores source."""
|
|
59
|
+
# Setup config with default .vault
|
|
60
|
+
Path('.vaulttool.yml').write_text(f"""
|
|
61
|
+
options:
|
|
62
|
+
suffix: .vault
|
|
63
|
+
key_file: {self.key_file.absolute()}
|
|
64
|
+
use_suffix_fallback: true
|
|
65
|
+
|
|
66
|
+
include_directories: [.]
|
|
67
|
+
exclude_directories: []
|
|
68
|
+
include_patterns:
|
|
69
|
+
- "*.env"
|
|
70
|
+
exclude_patterns: []
|
|
71
|
+
""")
|
|
72
|
+
|
|
73
|
+
# Create and encrypt with environment variable suffix
|
|
74
|
+
Path('development/creds.env').write_text('DATABASE_PASSWORD=super_secret\n')
|
|
75
|
+
os.environ['VAULTTOOL_OPTIONS_SUFFIX'] = 'n3test.vault'
|
|
76
|
+
|
|
77
|
+
vt = VaultTool()
|
|
78
|
+
vt.encrypt_task(force=True)
|
|
79
|
+
|
|
80
|
+
# Verify vault file created with correct name
|
|
81
|
+
assert Path('development/creds.env_n3test.vault').exists()
|
|
82
|
+
source_file = Path('development/creds.env')
|
|
83
|
+
assert source_file.exists()
|
|
84
|
+
|
|
85
|
+
# Delete source file
|
|
86
|
+
source_file.unlink()
|
|
87
|
+
|
|
88
|
+
# Run refresh - should restore development/creds.env
|
|
89
|
+
result = vt.refresh_task(force=True)
|
|
90
|
+
|
|
91
|
+
# Verify success
|
|
92
|
+
assert result['succeeded'] == 1
|
|
93
|
+
assert result['failed'] == 0
|
|
94
|
+
|
|
95
|
+
# CRITICAL: Check that the CORRECT file was restored
|
|
96
|
+
assert Path('development/creds.env').exists(), "Source file not restored"
|
|
97
|
+
assert Path('development/creds.env').read_text() == 'DATABASE_PASSWORD=super_secret\n'
|
|
98
|
+
|
|
99
|
+
# CRITICAL: Check that no wrong files were created
|
|
100
|
+
wrong_files = list(Path('development').glob('*_n3test'))
|
|
101
|
+
assert len(wrong_files) == 0, f"Wrong file created: {wrong_files}"
|
|
102
|
+
|
|
103
|
+
def test_infer_vault_suffix_with_various_file_extensions(self):
|
|
104
|
+
"""Test that infer_vault_suffix correctly handles different file types."""
|
|
105
|
+
# Use .custom.vault as current suffix for this test
|
|
106
|
+
Path('.vaulttool.yml').write_text(f"""
|
|
107
|
+
options:
|
|
108
|
+
suffix: .custom.vault
|
|
109
|
+
key_file: {self.key_file.absolute()}
|
|
110
|
+
use_suffix_fallback: true
|
|
111
|
+
|
|
112
|
+
include_directories: [.]
|
|
113
|
+
exclude_directories: []
|
|
114
|
+
include_patterns:
|
|
115
|
+
- "*.env"
|
|
116
|
+
- "*.ini"
|
|
117
|
+
- "*.yaml"
|
|
118
|
+
exclude_patterns: []
|
|
119
|
+
""")
|
|
120
|
+
|
|
121
|
+
vt = VaultTool()
|
|
122
|
+
|
|
123
|
+
# Test cases: (filename, expected_inferred_suffix)
|
|
124
|
+
test_cases = [
|
|
125
|
+
('config.env.vault', '.vault'), # Common .env extension
|
|
126
|
+
('config.ini.vault', '.vault'), # Common .ini extension
|
|
127
|
+
('config.yaml.vault', '.vault'), # Common .yaml extension
|
|
128
|
+
('config.yml.vault', '.vault'), # Common .yml extension
|
|
129
|
+
('config.prod.vault', '.prod.vault'), # Custom suffix (no common ext)
|
|
130
|
+
('database.cfg.vault', '.vault'), # Common .cfg extension
|
|
131
|
+
('secret.conf.vault', '.vault'), # Common .conf extension
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
for filename, expected_suffix in test_cases:
|
|
135
|
+
inferred = vt.infer_vault_suffix(filename)
|
|
136
|
+
assert inferred == expected_suffix, \
|
|
137
|
+
f"For {filename}: expected {expected_suffix!r}, got {inferred!r}"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == '__main__':
|
|
141
|
+
pytest.main([__file__, '-v'])
|
|
142
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|