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/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""VaultTool - Secure file encryption for secrets and configuration files.
|
|
2
|
+
|
|
3
|
+
This package provides secure encryption and management of sensitive files using
|
|
4
|
+
AES-256-CBC encryption with HMAC authentication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
__version__ = "2.0.0"
|
|
12
|
+
|
|
13
|
+
# Default logger for the package
|
|
14
|
+
_logger: Optional[logging.Logger] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_logger(name: str = "vaulttool") -> logging.Logger:
|
|
18
|
+
"""Get or create the VaultTool logger.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
name: Logger name (default: "vaulttool")
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Configured logger instance
|
|
25
|
+
"""
|
|
26
|
+
return logging.getLogger(name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def setup_logging(
|
|
30
|
+
level: int = logging.INFO,
|
|
31
|
+
format_string: Optional[str] = None,
|
|
32
|
+
include_timestamp: bool = True,
|
|
33
|
+
) -> logging.Logger:
|
|
34
|
+
"""Configure logging for VaultTool.
|
|
35
|
+
|
|
36
|
+
Sets up the root vaulttool logger with appropriate handlers and formatters.
|
|
37
|
+
This should be called once at application startup, typically from the CLI.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
41
|
+
format_string: Custom format string. If None, uses default format.
|
|
42
|
+
include_timestamp: Whether to include timestamps in log messages
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Configured logger instance
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> from vaulttool import setup_logging
|
|
49
|
+
>>> import logging
|
|
50
|
+
>>> logger = setup_logging(level=logging.DEBUG)
|
|
51
|
+
>>> logger.info("VaultTool initialized")
|
|
52
|
+
"""
|
|
53
|
+
logger = logging.getLogger("vaulttool")
|
|
54
|
+
logger.setLevel(level)
|
|
55
|
+
|
|
56
|
+
# Remove existing handlers to avoid duplicates
|
|
57
|
+
logger.handlers.clear()
|
|
58
|
+
|
|
59
|
+
# Create console handler
|
|
60
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
61
|
+
console_handler.setLevel(level)
|
|
62
|
+
|
|
63
|
+
# Create formatter
|
|
64
|
+
if format_string is None:
|
|
65
|
+
if include_timestamp:
|
|
66
|
+
format_string = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
67
|
+
else:
|
|
68
|
+
# Simpler format without timestamp (better for CLI usage)
|
|
69
|
+
format_string = "%(levelname)s: %(message)s"
|
|
70
|
+
|
|
71
|
+
formatter = logging.Formatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
|
|
72
|
+
console_handler.setFormatter(formatter)
|
|
73
|
+
|
|
74
|
+
# Add handler to logger
|
|
75
|
+
logger.addHandler(console_handler)
|
|
76
|
+
|
|
77
|
+
# Prevent propagation to root logger to avoid duplicate messages
|
|
78
|
+
logger.propagate = False
|
|
79
|
+
|
|
80
|
+
global _logger
|
|
81
|
+
_logger = logger
|
|
82
|
+
|
|
83
|
+
return logger
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# For backward compatibility and convenience
|
|
87
|
+
__all__ = [
|
|
88
|
+
"__version__",
|
|
89
|
+
"get_logger",
|
|
90
|
+
"setup_logging",
|
|
91
|
+
]
|
vaulttool/__main__.py
ADDED
vaulttool/cli.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
|
|
2
|
+
"""Command-line interface for VaultTool.
|
|
3
|
+
|
|
4
|
+
This module provides the command-line interface for VaultTool using Typer.
|
|
5
|
+
It exposes the main VaultTool functionality through a set of subcommands
|
|
6
|
+
for encrypting, decrypting, and managing vault files.
|
|
7
|
+
|
|
8
|
+
Available commands:
|
|
9
|
+
- encrypt: Encrypt source files to vault files
|
|
10
|
+
- refresh: Decrypt vault files to restore source files
|
|
11
|
+
- remove: Delete all vault files
|
|
12
|
+
- check-ignore: Validate .gitignore entries for source files
|
|
13
|
+
- version: Display VaultTool version information
|
|
14
|
+
- gen-vaulttool: Generate example configuration file
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
import typer
|
|
20
|
+
from . import setup_logging, get_logger
|
|
21
|
+
from .core import VaultTool
|
|
22
|
+
|
|
23
|
+
# Create app with proper help text
|
|
24
|
+
app = typer.Typer(
|
|
25
|
+
help="""Secure file encryption for secrets and configuration files.
|
|
26
|
+
|
|
27
|
+
Encrypts sensitive files using AES-256-CBC and manages their encrypted counterparts.
|
|
28
|
+
|
|
29
|
+
Key Options:
|
|
30
|
+
encrypt --force Re-encrypt all files (ignores checksums)
|
|
31
|
+
refresh --no-force Only restore missing files
|
|
32
|
+
|
|
33
|
+
Config: .vaulttool.yml (current dir) or ~/.vaulttool/.vaulttool.yml
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
vaulttool gen-vaulttool > .vaulttool.yml # Generate config file
|
|
37
|
+
vaulttool encrypt # Encrypt changed files only
|
|
38
|
+
vaulttool refresh # Restore all source files
|
|
39
|
+
vaulttool remove # Delete all vault files
|
|
40
|
+
|
|
41
|
+
vaulttool --verbose encrypt # Show detailed debug logs
|
|
42
|
+
vaulttool --quiet refresh # Show errors only
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Global options for logging control
|
|
47
|
+
verbose_option = typer.Option(False, "--verbose", "-v", help="Enable verbose debug logging")
|
|
48
|
+
quiet_option = typer.Option(False, "--quiet", "-q", help="Show only errors (suppress info/warning)")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _setup_cli_logging(verbose: bool = False, quiet: bool = False) -> logging.Logger:
|
|
52
|
+
"""Configure logging for CLI based on verbosity flags.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
verbose: Enable DEBUG level logging
|
|
56
|
+
quiet: Show only ERROR and CRITICAL messages
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Configured logger instance
|
|
60
|
+
"""
|
|
61
|
+
if verbose and quiet:
|
|
62
|
+
typer.echo("Warning: --verbose and --quiet are mutually exclusive. Using --verbose.", err=True)
|
|
63
|
+
quiet = False
|
|
64
|
+
|
|
65
|
+
if verbose:
|
|
66
|
+
level = logging.DEBUG
|
|
67
|
+
elif quiet:
|
|
68
|
+
level = logging.ERROR
|
|
69
|
+
else:
|
|
70
|
+
level = logging.INFO
|
|
71
|
+
|
|
72
|
+
return setup_logging(level=level, include_timestamp=False)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _get_version() -> str:
|
|
76
|
+
"""Get the version of vaulttool package."""
|
|
77
|
+
logger = get_logger(__name__)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
from importlib.metadata import version
|
|
81
|
+
pkg_version = version("vaulttool")
|
|
82
|
+
logger.debug(f"Retrieved version from importlib.metadata: {pkg_version}")
|
|
83
|
+
return pkg_version
|
|
84
|
+
except ImportError:
|
|
85
|
+
# importlib.metadata not available in older Python versions
|
|
86
|
+
logger.debug("importlib.metadata not available, trying fallback")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
# Package not found or other metadata error
|
|
89
|
+
logger.warning(f"Failed to get version from metadata: {e}")
|
|
90
|
+
|
|
91
|
+
# Fallback - try to read from pyproject.toml if available
|
|
92
|
+
try:
|
|
93
|
+
from pathlib import Path
|
|
94
|
+
|
|
95
|
+
# Look for pyproject.toml in parent directories
|
|
96
|
+
current_dir = Path(__file__).parent
|
|
97
|
+
logger.debug(f"Looking for pyproject.toml starting from {current_dir}")
|
|
98
|
+
|
|
99
|
+
for level in range(3): # Check up to 3 levels up
|
|
100
|
+
pyproject_path = current_dir / "pyproject.toml"
|
|
101
|
+
if pyproject_path.exists():
|
|
102
|
+
logger.debug(f"Found pyproject.toml at {pyproject_path}")
|
|
103
|
+
with open(pyproject_path, "r") as f:
|
|
104
|
+
content = f.read()
|
|
105
|
+
# Simple parsing for version
|
|
106
|
+
for line in content.split('\n'):
|
|
107
|
+
if line.strip().startswith('version = "'):
|
|
108
|
+
fallback_version = line.split('"')[1]
|
|
109
|
+
logger.debug(f"Extracted version from pyproject.toml: {fallback_version}")
|
|
110
|
+
return fallback_version
|
|
111
|
+
current_dir = current_dir.parent
|
|
112
|
+
logger.debug(f"Level {level + 1}: No pyproject.toml found, checking parent")
|
|
113
|
+
|
|
114
|
+
except (IOError, OSError) as e:
|
|
115
|
+
logger.warning(f"Failed to read pyproject.toml: {e}")
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.warning(f"Unexpected error reading pyproject.toml: {e}")
|
|
118
|
+
|
|
119
|
+
logger.debug("Using development version fallback")
|
|
120
|
+
return "unknown (development version)"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command("version")
|
|
124
|
+
def version_cmd(
|
|
125
|
+
verbose: bool = verbose_option,
|
|
126
|
+
quiet: bool = quiet_option,
|
|
127
|
+
):
|
|
128
|
+
"""Display VaultTool version information.
|
|
129
|
+
|
|
130
|
+
Shows the currently installed version of VaultTool along with
|
|
131
|
+
basic package information.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
verbose: Enable verbose debug logging
|
|
135
|
+
quiet: Show only errors
|
|
136
|
+
"""
|
|
137
|
+
_setup_cli_logging(verbose, quiet)
|
|
138
|
+
pkg_version = _get_version()
|
|
139
|
+
typer.echo(f"VaultTool version {pkg_version}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command("gen-vaulttool")
|
|
143
|
+
def gen_config():
|
|
144
|
+
"""Generate an example .vaulttool.yml configuration file.
|
|
145
|
+
|
|
146
|
+
Displays a formatted example configuration file that can be saved as
|
|
147
|
+
.vaulttool.yml to configure VaultTool for your project. The configuration
|
|
148
|
+
includes all available options with comments explaining their purpose.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
vaulttool gen-vaulttool > .vaulttool.yml
|
|
152
|
+
"""
|
|
153
|
+
example_config = """---
|
|
154
|
+
# .vaulttool.yml - VaultTool Configuration File
|
|
155
|
+
#
|
|
156
|
+
# This configuration file defines how VaultTool handles file encryption.
|
|
157
|
+
# Save this as .vaulttool.yml in your project root directory.
|
|
158
|
+
#
|
|
159
|
+
# Configuration file search order:
|
|
160
|
+
# 1. ./.vaulttool.yml (current directory)
|
|
161
|
+
# 2. ~/.vaulttool/.vaulttool.yml (user home)
|
|
162
|
+
# 3. /etc/vaulttool/config.yml (system-wide)
|
|
163
|
+
|
|
164
|
+
vaulttool:
|
|
165
|
+
# Directories to search for files to encrypt
|
|
166
|
+
# Defaults to current directory if empty
|
|
167
|
+
include_directories:
|
|
168
|
+
- "."
|
|
169
|
+
|
|
170
|
+
# Directories to exclude from encryption
|
|
171
|
+
exclude_directories:
|
|
172
|
+
- "__pycache__"
|
|
173
|
+
- ".git"
|
|
174
|
+
- ".pytest_cache"
|
|
175
|
+
- ".venv"
|
|
176
|
+
- "dist"
|
|
177
|
+
- "node_modules"
|
|
178
|
+
|
|
179
|
+
# File patterns to include for encryption
|
|
180
|
+
include_patterns:
|
|
181
|
+
- "*.conf" # Config files
|
|
182
|
+
- "*.env" # Environment files
|
|
183
|
+
- "*.ini" # Configuration files
|
|
184
|
+
- "*.json" # JSON config files
|
|
185
|
+
- "*.yaml" # YAML config files
|
|
186
|
+
- "*.yml" # YAML config files
|
|
187
|
+
|
|
188
|
+
# File patterns to exclude from encryption
|
|
189
|
+
exclude_patterns:
|
|
190
|
+
- "*.log" # Log files
|
|
191
|
+
- "*.tmp" # Temporary files
|
|
192
|
+
- "*.vault" # Existing vault files
|
|
193
|
+
- "*example*" # Example files
|
|
194
|
+
- "*sample*" # Sample files
|
|
195
|
+
|
|
196
|
+
# Encryption options
|
|
197
|
+
options:
|
|
198
|
+
# Suffix added to encrypted files (e.g., config.env -> config.env.vault)
|
|
199
|
+
suffix: ".vault"
|
|
200
|
+
|
|
201
|
+
# Full Path to encryption key file
|
|
202
|
+
key_file: "/home/USERNAME/.vaulttool/vault.key"
|
|
203
|
+
"""
|
|
204
|
+
typer.echo(example_config.strip())
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@app.command()
|
|
208
|
+
def remove(
|
|
209
|
+
verbose: bool = verbose_option,
|
|
210
|
+
quiet: bool = quiet_option,
|
|
211
|
+
):
|
|
212
|
+
"""Remove all vault files matching the configured suffix.
|
|
213
|
+
|
|
214
|
+
This command will permanently delete all .vault files found in the configured
|
|
215
|
+
include directories that match the suffix pattern. This operation cannot be undone.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
verbose: Enable verbose debug logging
|
|
219
|
+
quiet: Show only errors
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
Exit code 1 if any operations failed.
|
|
223
|
+
"""
|
|
224
|
+
_setup_cli_logging(verbose, quiet)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
vt = VaultTool()
|
|
228
|
+
result = vt.remove_task()
|
|
229
|
+
|
|
230
|
+
# Display summary
|
|
231
|
+
if not quiet:
|
|
232
|
+
typer.echo("\n" + "="*60)
|
|
233
|
+
typer.echo("Remove Summary:")
|
|
234
|
+
typer.echo(f" Total: {result['total']}")
|
|
235
|
+
typer.echo(f" Removed: {result['removed']}")
|
|
236
|
+
typer.echo(f" Failed: {result['failed']}")
|
|
237
|
+
typer.echo("="*60)
|
|
238
|
+
|
|
239
|
+
if result['failed'] > 0:
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
except Exception as e:
|
|
243
|
+
typer.echo(f"ERROR: {e}", err=True)
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@app.command()
|
|
248
|
+
def encrypt(
|
|
249
|
+
force: bool = typer.Option(False, "--force", help="Re-encrypt and overwrite existing .vault files"),
|
|
250
|
+
verbose: bool = verbose_option,
|
|
251
|
+
quiet: bool = quiet_option,
|
|
252
|
+
):
|
|
253
|
+
"""Encrypt files as configured.
|
|
254
|
+
|
|
255
|
+
Encrypts all source files matching the configured patterns into vault files.
|
|
256
|
+
By default, only encrypts files that have changed (different HMAC) or
|
|
257
|
+
don't have existing vault files.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
force: If True, re-encrypt and overwrite existing .vault files even if
|
|
261
|
+
the source file hasn't changed.
|
|
262
|
+
verbose: Enable verbose debug logging
|
|
263
|
+
quiet: Show only errors
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
Exit code 1 if any operations failed.
|
|
267
|
+
"""
|
|
268
|
+
_setup_cli_logging(verbose, quiet)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
vt = VaultTool()
|
|
272
|
+
result = vt.encrypt_task(force=force)
|
|
273
|
+
|
|
274
|
+
# Display summary
|
|
275
|
+
if not quiet:
|
|
276
|
+
typer.echo("\n" + "="*60)
|
|
277
|
+
typer.echo("Encrypt Summary:")
|
|
278
|
+
typer.echo(f" Total: {result['total']}")
|
|
279
|
+
typer.echo(f" Created: {result['created']}")
|
|
280
|
+
typer.echo(f" Updated: {result['updated']}")
|
|
281
|
+
typer.echo(f" Skipped: {result['skipped']}")
|
|
282
|
+
typer.echo(f" Failed: {result['failed']}")
|
|
283
|
+
typer.echo("="*60)
|
|
284
|
+
|
|
285
|
+
if result['failed'] > 0:
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
typer.echo(f"ERROR: {e}", err=True)
|
|
290
|
+
sys.exit(1)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.command()
|
|
294
|
+
def refresh(
|
|
295
|
+
force: bool = typer.Option(
|
|
296
|
+
True,
|
|
297
|
+
"--force/--no-force",
|
|
298
|
+
help="Overwrite plaintext files from existing .vault files",
|
|
299
|
+
),
|
|
300
|
+
verbose: bool = verbose_option,
|
|
301
|
+
quiet: bool = quiet_option,
|
|
302
|
+
):
|
|
303
|
+
"""Restore/refresh plaintext files from .vault files.
|
|
304
|
+
|
|
305
|
+
Decrypts vault files to restore their corresponding source files.
|
|
306
|
+
By default, overwrites existing source files (force=True).
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
force: If True (default), overwrite existing plaintext files.
|
|
310
|
+
If False, only restore missing files.
|
|
311
|
+
verbose: Enable verbose debug logging
|
|
312
|
+
quiet: Show only errors
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
Exit code 1 if any operations failed.
|
|
316
|
+
"""
|
|
317
|
+
_setup_cli_logging(verbose, quiet)
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
vt = VaultTool()
|
|
321
|
+
result = vt.refresh_task(force=force)
|
|
322
|
+
|
|
323
|
+
# Display summary
|
|
324
|
+
if not quiet:
|
|
325
|
+
typer.echo("\n" + "="*60)
|
|
326
|
+
typer.echo("Refresh Summary:")
|
|
327
|
+
typer.echo(f" Total: {result['total']}")
|
|
328
|
+
typer.echo(f" Succeeded: {result['succeeded']}")
|
|
329
|
+
typer.echo(f" Failed: {result['failed']}")
|
|
330
|
+
typer.echo(f" Skipped: {result['skipped']}")
|
|
331
|
+
typer.echo("="*60)
|
|
332
|
+
|
|
333
|
+
if result['failed'] > 0:
|
|
334
|
+
sys.exit(1)
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
typer.echo(f"ERROR: {e}", err=True)
|
|
338
|
+
sys.exit(1)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@app.command()
|
|
342
|
+
def check_ignore(
|
|
343
|
+
verbose: bool = verbose_option,
|
|
344
|
+
quiet: bool = quiet_option,
|
|
345
|
+
):
|
|
346
|
+
"""Check that all plaintext files are ignored by Git.
|
|
347
|
+
|
|
348
|
+
Validates that all source files matching the configured patterns are
|
|
349
|
+
properly added to .gitignore to prevent accidental commits of sensitive data.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
verbose: Enable verbose debug logging
|
|
353
|
+
quiet: Show only errors
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Exit code 0 if operation completed.
|
|
357
|
+
"""
|
|
358
|
+
_setup_cli_logging(verbose, quiet)
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
vt = VaultTool()
|
|
362
|
+
vt.check_ignore_task()
|
|
363
|
+
except Exception as e:
|
|
364
|
+
typer.echo(f"ERROR: {e}", err=True)
|
|
365
|
+
sys.exit(1)
|