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