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.
Files changed (25) hide show
  1. {vaulttool-2.0.1 → vaulttool-2.0.2}/PKG-INFO +5 -4
  2. {vaulttool-2.0.1 → vaulttool-2.0.2}/pyproject.toml +4 -4
  3. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/core.py +82 -3
  4. vaulttool-2.0.2/vaulttool/tests/test_suffix_switch_bug.py +142 -0
  5. {vaulttool-2.0.1 → vaulttool-2.0.2}/LICENSE +0 -0
  6. {vaulttool-2.0.1 → vaulttool-2.0.2}/README.md +0 -0
  7. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/__init__.py +0 -0
  8. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/__main__.py +0 -0
  9. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/cli.py +0 -0
  10. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/config.py +0 -0
  11. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/conftest.py +0 -0
  12. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_checksum_trigger.py +0 -0
  13. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_config.py +0 -0
  14. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_core.py +0 -0
  15. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_decrypt_missing_sources.py +0 -0
  16. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_env_config.py +0 -0
  17. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_error_aggregation.py +0 -0
  18. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_gitignore.py +0 -0
  19. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_key_file_validation.py +0 -0
  20. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_main_integration.py +0 -0
  21. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_priority3_fixes.py +0 -0
  22. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_security_and_edge_cases.py +0 -0
  23. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_suffix_fallback.py +0 -0
  24. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/tests/test_utils.py +0 -0
  25. {vaulttool-2.0.1 → vaulttool-2.0.2}/vaulttool/utils.py +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: vaulttool
3
- Version: 2.0.1
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.1"
3
+ version = "2.0.2"
4
4
  description = "Secure file encryption for secrets/configs"
5
- authors = ["Josef Fuchs"]
6
- license = { text = "Apache-2" }
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.1"
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
- source = self.source_filename(str(vault_file), ".vault")
457
- # Only use .vault as fallback if custom suffix doesn't exist
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
- vault_suffix = ".vault"
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