pilfer 1.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.
pilfer/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Pilfer - Bulk De/recrypt all ansible vault files in a project recursively for search/editing.
3
+
4
+ This package provides a command-line tool for decrypting all Ansible vault files
5
+ in a project recursively for search/editing, then re-encrypting them when done.
6
+
7
+ Borrows from the excellent, but no longer supported Ansible Toolkit (https://github.com/dellis23/ansible-toolkit)
8
+ """
9
+
10
+ __version__ = "1.0.0"
11
+ __author__ = "Tom Paine"
12
+ __email__ = "github@aioue.net"
13
+ __description__ = "Decrypt all ansible vault files in a project recursively for search/editing, then re-encrypt when done."
pilfer/cli.py ADDED
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ # heavily borrows from this excellent repo https://github.com/dellis23/ansible-toolkit
4
+
5
+ # pilfer - decrypt all ansible vault files recursively for search/editing
6
+ # pilfer [open|close]
7
+
8
+ import argparse
9
+ import errno
10
+ import hashlib
11
+ import json
12
+ import os
13
+ import shutil
14
+ import configparser
15
+ from pathlib import Path
16
+
17
+ # Use Ansible's official vault implementation instead of third-party library
18
+ from ansible.constants import DEFAULT_VAULT_ID_MATCH
19
+ from ansible.parsing.vault import VaultLib, VaultSecret
20
+
21
+
22
+ temp_vault_file_list_path = "vaultedFileList.json"
23
+ list_of_vault_encrypted_files = []
24
+ temp_hidden_encrypted_copies_directory_path = '.vault'
25
+
26
+
27
+ def get_vault_password_file():
28
+ """Get vault password file from ansible.cfg or fall back to default locations"""
29
+ # First try to read from ansible.cfg
30
+ try:
31
+ config = configparser.ConfigParser()
32
+ config.read('ansible.cfg')
33
+ if 'defaults' in config and 'vault_password_file' in config['defaults']:
34
+ vault_file = config['defaults']['vault_password_file']
35
+ # Expand tilde for home directory
36
+ vault_file = os.path.expanduser(vault_file)
37
+ if os.path.exists(vault_file):
38
+ return vault_file
39
+ except Exception:
40
+ pass
41
+
42
+ # Fall back to common locations
43
+ fallback_locations = [
44
+ "../../vault_password_file", # Original default
45
+ "~/.ansible-vault/.vault-file", # Common location
46
+ ".vault_password", # Local project file
47
+ "vault_password_file" # Simple local file
48
+ ]
49
+
50
+ for location in fallback_locations:
51
+ expanded_location = os.path.expanduser(location)
52
+ if os.path.exists(expanded_location):
53
+ return expanded_location
54
+
55
+ raise FileNotFoundError("Could not find vault password file. Please ensure it exists or specify with -p argument.")
56
+
57
+
58
+ # find all files that have the ansible vault header and write it to disk
59
+ def write_vaulted_file_list():
60
+ walk_dir = os.path.abspath(os.getcwd())
61
+
62
+ for dirpath, dirnames, filenames in os.walk(walk_dir):
63
+
64
+ for name in filenames:
65
+ # print name
66
+
67
+ # disable filename filter since we might have encrypted everything
68
+ # yamlExtensions = ('yml','yaml')
69
+ # if name.endswith(yamlExtensions):
70
+ # print name
71
+
72
+ # full path
73
+ filePath = os.path.join(dirpath, name)
74
+ # print filePath
75
+
76
+ # find all files with the ansible vault header
77
+ try:
78
+ with open(filePath, 'rb') as open_file:
79
+ first_line = open_file.readline()
80
+
81
+ if first_line.startswith(b'$ANSIBLE_VAULT;'):
82
+ # print filePath
83
+ list_of_vault_encrypted_files.append(filePath)
84
+ except (IOError, OSError, PermissionError):
85
+ # Skip files we can't read
86
+ continue
87
+
88
+ # print vaultedFileList
89
+ with open(temp_vault_file_list_path, 'w') as open_file:
90
+ json.dump(list_of_vault_encrypted_files, open_file, indent=2)
91
+
92
+
93
+ def decrypt_vault_files(vault_password_file_path=None):
94
+ # load the list of encrypted files
95
+ with open(temp_vault_file_list_path, 'r') as vaultListFile:
96
+ vaultedFileList = json.load(vaultListFile)
97
+
98
+ # list the detected encrypted files
99
+ # print json.dumps(vaultedFileList, indent=2)
100
+
101
+ # determine vault password file
102
+ if vault_password_file_path:
103
+ vault_file = vault_password_file_path
104
+ else:
105
+ vault_file = get_vault_password_file()
106
+
107
+ # load vault password into memory
108
+ with open(vault_file, 'r') as vault_password_file:
109
+ vaultPassword = vault_password_file.read().strip()
110
+
111
+ # create VaultLib instance using Ansible's official implementation
112
+ vault = VaultLib([
113
+ (DEFAULT_VAULT_ID_MATCH, VaultSecret(vaultPassword.encode('utf-8')))
114
+ ])
115
+
116
+ # iterate over the list of vaulted files
117
+ for vaultedFilePath in vaultedFileList:
118
+ try:
119
+ # recursively build a mirror directory structure for this file
120
+ mkdir_p(os.path.join(temp_hidden_encrypted_copies_directory_path + vaultedFilePath))
121
+
122
+ # make a copy of the encrypted file
123
+ shutil.copy2(vaultedFilePath, temp_hidden_encrypted_copies_directory_path + vaultedFilePath + '/encrypted')
124
+
125
+ # decrypt the file using Ansible's official vault implementation
126
+ # Read encrypted data as bytes to preserve exact formatting
127
+ with open(vaultedFilePath, 'rb') as f:
128
+ encrypted_data = f.read()
129
+ # VaultLib.decrypt() returns bytes, preserving binary data and line endings
130
+ decrypted_bytes = vault.decrypt(encrypted_data)
131
+
132
+ # write a hash of the decrypted content (bytes) to disk in the temporary directory
133
+ file_hash = hashlib.sha256(decrypted_bytes).hexdigest()
134
+ with open(temp_hidden_encrypted_copies_directory_path + vaultedFilePath + '/hash', 'w') as decryptedVaultFileHash:
135
+ decryptedVaultFileHash.write(file_hash)
136
+
137
+ # write the decrypted data to disk as bytes to preserve exact formatting
138
+ with open(vaultedFilePath, 'wb') as decryptedVaultFile:
139
+ decryptedVaultFile.write(decrypted_bytes)
140
+
141
+ except Exception as e:
142
+ print(f"Failed to decrypt {vaultedFilePath}: {e}")
143
+ continue
144
+
145
+
146
+ def mkdir_p(path):
147
+ try:
148
+ os.makedirs(path)
149
+ except OSError as exc: # Python >2.5
150
+ if exc.errno == errno.EEXIST and os.path.isdir(path):
151
+ pass
152
+ else:
153
+ raise
154
+
155
+
156
+ def recrypt_vault_files(vault_password_file_path=None):
157
+ with open(temp_vault_file_list_path, 'r') as vaultListFile:
158
+ vaultedFileList = json.load(vaultListFile)
159
+
160
+ # determine vault password file
161
+ if vault_password_file_path:
162
+ vault_file = vault_password_file_path
163
+ else:
164
+ vault_file = get_vault_password_file()
165
+
166
+ # load vault password into memory
167
+ with open(vault_file, 'r') as vault_password_file:
168
+ vaultPassword = vault_password_file.read().strip()
169
+
170
+ # create VaultLib instance using Ansible's official implementation
171
+ vault = VaultLib([
172
+ (DEFAULT_VAULT_ID_MATCH, VaultSecret(vaultPassword.encode('utf-8')))
173
+ ])
174
+
175
+ # iterate over the list of vaulted files
176
+ for vaultedFilePath in vaultedFileList:
177
+ try:
178
+ # Load stored encrypted data as bytes
179
+ with open(temp_hidden_encrypted_copies_directory_path + vaultedFilePath + '/encrypted', 'rb') as f:
180
+ old_encrypted_data = f.read()
181
+
182
+ with open(temp_hidden_encrypted_copies_directory_path + vaultedFilePath + '/hash', 'r') as f:
183
+ old_hash = f.read().strip()
184
+
185
+ # Load (potentially) new data from original path as bytes
186
+ with open(vaultedFilePath, 'rb') as f:
187
+ new_data_bytes = f.read()
188
+ new_hash = hashlib.sha256(new_data_bytes).hexdigest()
189
+
190
+ # Determine whether to re-encrypt
191
+ if old_hash != new_hash:
192
+ # File was modified, re-encrypt it using Ansible's official vault implementation
193
+ # VaultLib.encrypt() expects and returns bytes
194
+ new_encrypted_data = vault.encrypt(new_data_bytes)
195
+ else:
196
+ # File unchanged, restore original encrypted version
197
+ new_encrypted_data = old_encrypted_data
198
+
199
+ # Update file with bytes to preserve exact formatting
200
+ with open(vaultedFilePath, 'wb') as f:
201
+ f.write(new_encrypted_data)
202
+
203
+ # Clean vault
204
+ try:
205
+ os.remove(temp_hidden_encrypted_copies_directory_path + vaultedFilePath + '/encrypted')
206
+ os.remove(temp_hidden_encrypted_copies_directory_path + vaultedFilePath + '/hash')
207
+ os.removedirs(temp_hidden_encrypted_copies_directory_path + vaultedFilePath)
208
+ except Exception as e:
209
+ print(f"Warning: Failed to clean temp files for {vaultedFilePath}: {e}")
210
+ except Exception as e:
211
+ print(f"Failed to process {vaultedFilePath}: {e}")
212
+ continue
213
+
214
+ try:
215
+ os.removedirs(temp_hidden_encrypted_copies_directory_path)
216
+ except Exception:
217
+ pass
218
+
219
+ try:
220
+ os.remove(temp_vault_file_list_path)
221
+ except Exception:
222
+ pass
223
+
224
+
225
+ def main():
226
+ """Main CLI entry point for pilfer"""
227
+ # Parse Args
228
+ parser = argparse.ArgumentParser(
229
+ prog='pilfer',
230
+ description='Decrypt all ansible vault files in a project recursively for search/editing, then re-encrypt them when done'
231
+ )
232
+ parser.add_argument('action', choices=['open', 'close'],
233
+ help="'open' to decrypt all vault files, 'close' to re-encrypt modified files")
234
+ parser.add_argument('-p', '--vault-password-file', type=str,
235
+ help="Path to vault password file")
236
+ args = parser.parse_args()
237
+
238
+ # Open / Close Vault
239
+ if args.action == 'open':
240
+ # check for an existing encrypted file list
241
+ # if one exists, decrypt the files
242
+ # if it doesn't, make one
243
+ if Path(temp_vault_file_list_path).is_file():
244
+ # print "path exists, skipping file creation"
245
+ decrypt_vault_files(args.vault_password_file)
246
+ else:
247
+ write_vaulted_file_list()
248
+ decrypt_vault_files(args.vault_password_file)
249
+
250
+ elif args.action == 'close':
251
+ recrypt_vault_files(args.vault_password_file)
252
+
253
+
254
+ if __name__ == '__main__':
255
+ main()
@@ -0,0 +1,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: pilfer
3
+ Version: 1.0.0
4
+ Summary: Decrypt all ansible vault files in a project recursively for search/editing, then re-encrypt when done.
5
+ Author-email: Tom Paine <github@aioue.net>
6
+ Project-URL: Homepage, https://github.com/aioue/pilfer
7
+ Project-URL: Repository, https://github.com/aioue/pilfer
8
+ Project-URL: Issues, https://github.com/aioue/pilfer/issues
9
+ Keywords: ansible,vault,encryption,devops,automation
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.6
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.6
26
+ Description-Content-Type: text/markdown
27
+ Requires-Dist: ansible>=2.9.0
28
+
29
+ # pilfer
30
+ Decrypt *all* ansible vault files in a project recursively for search/editing, then re-encrypt them all at once when you're done.
31
+
32
+ Borrows heavily from the excellent, but no longer supported [Ansible Toolkit](https://github.com/dellis23/ansible-toolkit).
33
+
34
+ **Updated for Python 3 compatibility with modern features and ansible.cfg integration.**
35
+
36
+ Tested with Ansible v2.18.x and Python 3.12.x
37
+
38
+ ## Features
39
+
40
+ - **Python 3 compatible** - Modernized for current Python versions
41
+ - **ansible.cfg integration** - Automatically reads `vault_password_file` from your ansible.cfg
42
+ - **Change detection** - Only re-encrypts files that were actually modified (using SHA256)
43
+ - **Safe operation** - Preserves original encrypted content for unchanged files
44
+ - **No third-party dependencies** - Uses Ansible's official vault implementation directly
45
+ - **Binary data preservation** - Preserves exact line endings and formatting (critical for certificates)
46
+
47
+ ## Usage
48
+ ```
49
+ pilfer [open|close] [-p VAULT_PASSWORD_FILE]
50
+ ```
51
+
52
+ ### Basic Usage
53
+
54
+ **Option 1: Standalone Script (No Installation)**
55
+ - Download `pilfer.py` and place it in your Ansible project directory
56
+ - Run `python pilfer.py open` to decrypt all vaulted files recursively
57
+ - Edit/search plaintext as needed
58
+ - Run `python pilfer.py close` to re-encrypt any changed files
59
+
60
+ **Option 2: Installed via pipx (Recommended)**
61
+ - Install pilfer via pipx: `pipx install pilfer`
62
+ - Run `pilfer open` to decrypt all vaulted files recursively
63
+ - Edit/search plaintext as needed
64
+ - Run `pilfer close` to re-encrypt any changed files
65
+
66
+ Any unchanged files will be returned to their original state.
67
+
68
+ ### Vault Password File Detection
69
+
70
+ The script automatically detects your vault password file in this order:
71
+
72
+ 1. **Command line argument**: `-p /path/to/vault/file`
73
+ 2. **ansible.cfg**: Reads `vault_password_file` from `[defaults]` section
74
+ 3. **Common locations**:
75
+ - `~/.ansible-vault/.vault-file`
76
+ - `../../vault_password_file`
77
+ - `.vault_password`
78
+ - `vault_password_file`
79
+
80
+ ### Examples
81
+
82
+ **Using the installed version:**
83
+ ```bash
84
+ # Use ansible.cfg vault_password_file setting (recommended)
85
+ pilfer open
86
+
87
+ # Specify custom vault password file
88
+ pilfer open -p ~/.my-vault-password
89
+
90
+ # Close and re-encrypt modified files
91
+ pilfer close
92
+ ```
93
+
94
+ **Using the standalone script:**
95
+ ```bash
96
+ # Use ansible.cfg vault_password_file setting (recommended)
97
+ python pilfer.py open
98
+
99
+ # Specify custom vault password file
100
+ python pilfer.py open -p ~/.my-vault-password
101
+
102
+ # Close and re-encrypt modified files
103
+ python pilfer.py close
104
+ ```
105
+
106
+ ## Installation
107
+
108
+ ### Option 1: Standalone Script (No Installation Required)
109
+
110
+ Download and use the standalone script directly:
111
+
112
+ ```bash
113
+ # Download the standalone script
114
+ curl -O https://raw.githubusercontent.com/aioue/pilfer/main/pilfer.py
115
+
116
+ # Make it executable (required for ./pilfer.py usage)
117
+ chmod +x pilfer.py
118
+
119
+ # Use it directly
120
+ ./pilfer.py open
121
+ # OR
122
+ python pilfer.py open
123
+ ```
124
+
125
+ ### Option 2: Install via pipx (Recommended for Regular Use)
126
+
127
+ **Python 3.6+** is required. Install pilfer using pipx for isolated CLI tool management:
128
+
129
+ ```bash
130
+ # Install pilfer via pipx (recommended)
131
+ pipx install pilfer
132
+
133
+ # Verify installation
134
+ pilfer --help
135
+ ```
136
+
137
+ ### Alternative Installation Methods
138
+
139
+ If you prefer other installation methods:
140
+
141
+ ```bash
142
+ # Install from source (in development mode)
143
+ git clone https://github.com/aioue/pilfer.git
144
+ cd pilfer
145
+ pip install -e .
146
+
147
+ # Direct pip installation (not recommended for CLI tools)
148
+ pip install pilfer
149
+ ```
150
+
151
+ ### Requirements
152
+
153
+ Pilfer requires **Ansible** to be available. If not already installed:
154
+
155
+ ```bash
156
+ # Using pipx (recommended for CLI tools)
157
+ pipx install ansible
158
+
159
+ # Using pip
160
+ pip install ansible
161
+
162
+ # System package manager
163
+ # Ubuntu/Debian:
164
+ sudo apt update && sudo apt install ansible
165
+
166
+ # RHEL/CentOS/Fedora:
167
+ sudo dnf install ansible
168
+
169
+ # macOS:
170
+ brew install ansible
171
+ ```
172
+
173
+ ### ansible.cfg Setup (Recommended)
174
+
175
+ Add to your `ansible.cfg`:
176
+ ```ini
177
+ [defaults]
178
+ vault_password_file = ~/.ansible-vault/.vault-file
179
+ ```
180
+
181
+ This eliminates the need to manually configure vault password paths.
182
+
183
+ ## Development and Publishing
184
+
185
+ ### For Developers
186
+
187
+ To set up for development:
188
+
189
+ ```bash
190
+ # Clone the repository
191
+ git clone https://github.com/aioue/pilfer.git
192
+ cd pilfer
193
+
194
+ # Install in development mode
195
+ pip install -e .
196
+
197
+ # Make changes and test
198
+ pilfer --help
199
+ ```
200
+
201
+ ### Publishing to PyPI
202
+
203
+ Prerequisites:
204
+ ```bash
205
+ # Install build tools
206
+ pip install build twine
207
+
208
+ # Configure PyPI credentials
209
+ # ~/.pypirc or use environment variables
210
+ ```
211
+
212
+ Build and publish:
213
+ ```bash
214
+ # Make the script executable
215
+ chmod +x build_and_publish.sh
216
+
217
+ # Publish to TestPyPI first
218
+ ./build_and_publish.sh test
219
+
220
+ # After testing, publish to production PyPI
221
+ ./build_and_publish.sh prod
222
+ ```
223
+
224
+ The build script will:
225
+ 1. Clean previous builds
226
+ 2. Build the package using modern Python packaging
227
+ 3. Upload to PyPI/TestPyPI using twine
228
+ 4. Provide installation instructions
@@ -0,0 +1,7 @@
1
+ pilfer/__init__.py,sha256=3N7TCsFQL2mNLlrM2WJT2c8tPo7km-zwU16n7KIkUtU,584
2
+ pilfer/cli.py,sha256=cjJbjDWR0eXNq7DnldZIY2zTd_RgySnc6ur-BfDF-KE,9709
3
+ pilfer-1.0.0.dist-info/METADATA,sha256=VtKu9hZkuiLm7jqTo6y3ULzQo5ZKYXdadBeBhuWYmu4,6113
4
+ pilfer-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
5
+ pilfer-1.0.0.dist-info/entry_points.txt,sha256=z0AIRa_eNRWZUUqPzZc2nQNpYYXI3sKvxjpPO-KKiRI,43
6
+ pilfer-1.0.0.dist-info/top_level.txt,sha256=hl-KQi0BEX35OJTEv3Fl0HKr1k0mRzXtPHrJyur0tCM,7
7
+ pilfer-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pilfer = pilfer.cli:main
@@ -0,0 +1 @@
1
+ pilfer