python4cpm 1.0.10__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gonzalo Atienza Rela
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: python4cpm
3
+ Version: 1.0.10
4
+ Summary: Python for CPM
5
+ Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Gonzalo Atienza Rela
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in
18
+ all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
+ THE SOFTWARE.
27
+
28
+ Requires-Python: >=3.8
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Provides-Extra: dev
32
+ Requires-Dist: ruff; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Python4CPM
36
+
37
+ A simple way of using python scripts with CyberArk CPM rotations. This module levereages the [Terminal Plugin Controller](https://docs.cyberark.com/pam-self-hosted/latest/en/content/pasimp/plug-in-terminal-plugin-controller.htm) (TPC) in CPM to offload a password rotation logic into a script.
38
+
39
+ This platform allows you to duplicate it multiple times, simply changing its settings from Privilege Cloud/PVWA to point to different python scripts leveraging the module included with the base platform.
40
+
41
+ ## Installation
42
+
43
+ ### Preparing Python
44
+
45
+ 1. Install Python in CPM. **Python must be installed for all users when running the install wizard**.
46
+ 2. Create a venv in CPM, by running `py -m venv c:\venv`. Use the default location `c:\venv` or a custom one (e.g., `c:\my-venv-path`).
47
+ 3. Install `python4cpm` in your venv:
48
+ - If your CPM can connect to the internet, install with `c:\venv\Scripts\pip install python4cpm`.
49
+ - If your CPM cannot connect to the internet:
50
+ - Download the [latest wheel](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-wheel.zip).
51
+ - Copy the file to CPM, extract to a temporary location.
52
+ - From the temporary location run `c:\venv\Scripts\pip install --no-index --find-links=.\python4cpm-wheel python4cpm`.
53
+
54
+
55
+ ### Importing the platform
56
+
57
+ 1. Download the [latest platform zip file](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-platform.zip).
58
+ 2. Import the platform zip file into Privilege Cloud/PVWA `(Administration -> Platform Management -> Import platform)`.
59
+ 3. Craft your python script and place it within the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
60
+ 4. Duplicate the imported platform in Privilege Cloud/PVWA `(Administration -> Platform Management -> Application -> Python for CPM)` and name it after your application (e.g., My App).
61
+ 5. Edit the duplicated platform and specify the path of your placed script in the bin folder of CPM, under `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonScriptPath -> Value` (e.g., `bin\myapp.py`).
62
+ 6. If you used a custom venv location, also update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonExePath -> Value` with the custom path for the venv's `python.exe` file (e.g., `c:\my-venv-path\Scripts\python.exe`).
63
+ 7. If you want to disable logging, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging -> Value` to `no`.
64
+ 8. If you want to change the logging level to `debug`, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLoggingLevel -> Value` to `debug`.
65
+ 9. For new applications repeat steps from 3 to 8.
66
+
67
+
68
+ ## Python Script
69
+
70
+ ### Example:
71
+
72
+ ```python
73
+ from python4cpm import Python4CPM
74
+
75
+
76
+ p4cpm = Python4CPM("MyApp") # this instantiates the object and grabs all arguments and secrets shared by TPC
77
+
78
+ # These are the usable properties and related methods from the object:
79
+ p4cpm.args.action # action requested from CPM
80
+ p4cpm.args.address # address from the account address field
81
+ p4cpm.args.username # username from the account username field
82
+ p4cpm.args.reconcile_username # reconcile username from the linked reconcile account
83
+ p4cpm.args.logon_username # logon username from the linked logon account
84
+ p4cpm.args.logging # used to carry the platform logging settings for python
85
+ p4cpm.secrets.password.get() # get str from password received from the vault
86
+ p4cpm.secrets.new_password.get() # get str from new password in case of a rotation
87
+ p4cpm.secrets.logon_password.get() # get str from linked logon account password
88
+ p4cpm.secrets.reconcile_password.get() # get str from linked reconcile account password
89
+
90
+ # Logging methods -> Will only log if Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging is set to yes (default is yes)
91
+ p4cpm.log_error("this is an error message") # logs error into Logs/ThirdParty/Python4CPM/MyApp.log
92
+ p4cpm.log_warning("this is a warning message") # logs warning into Logs/ThirdParty/Python4CPM/MyApp.log
93
+ p4cpm.log_info("this is an info message") # logs info into Logs/ThirdParty/Python4CPM/MyApp.log
94
+ # Logging level -> Will only log debug messages if Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLoggingLevel is set to debug (default is info)
95
+ p4cpm.log_debug("this is an debug message") # logs info into Logs/ThirdParty/Python4CPM/MyApp.log if logging level is set to debug
96
+
97
+ # Terminate signals -> ALWAYS use one of the following three signals to terminate the script
98
+ ## p4cpm.close_success() # terminate with success state
99
+ ## p4cpm.close_fail() # terminate with recoverable failed state
100
+ ## p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
101
+ # If no signal is call, CPM will not know if the action was successful and display an error
102
+
103
+
104
+ # Verification example -> verify the username and password are valid
105
+ def verify(from_reconcile=False):
106
+ if from_reconcile is False:
107
+ pass
108
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.secrets.password.get()
109
+ # for your logic in a verification
110
+ else:
111
+ pass
112
+ # Use p4cpm.args.address, p4cpm.args.reconcile_username, p4cpm.secrets.reconcile_password.get()
113
+ # for your logic in a verification
114
+ result = True
115
+ if result is True:
116
+ p4cpm.log_info("verification successful") # logs info message into Logs/ThirdParty/Python4CPM/MyApp.log
117
+ else:
118
+ p4cpm.log_error("something went wrong") # logs error message Logs/ThirdParty/Python4CPM/MyApp.log
119
+ raise Exception("verify failed") # raise to trigger failed termination signal
120
+
121
+
122
+ # Rotation example -> rotate the password of the account
123
+ def change(from_reconcile=False):
124
+ if from_reconcile is False:
125
+ pass
126
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.secrets.password.get()
127
+ # and p4cpm.secrets.new_password.get() for your logic in a rotation
128
+ else:
129
+ pass
130
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.args.reconcile_username,
131
+ # p4cpm.secrets.reconcile_password.get() and p4cpm.secrets.new_password.get() for your logic in a reconciliation
132
+ result = True
133
+ if result is True:
134
+ p4cpm.log_info("rotation successful") # logs info message into Logs/ThirdParty/Python4CPM/MyApp.log
135
+ else:
136
+ p4cpm.log_error("something went wrong") # logs error message Logs/ThirdParty/Python4CPM/MyApp.log
137
+ raise Exception("change failed") # raise to trigger failed termination signal
138
+
139
+
140
+ if __name__ == "__main__":
141
+ try:
142
+ if action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
143
+ verify()
144
+ p4cpm.close_success() # terminate with success state
145
+ elif p4cpm.args.action == Python4CPM.ACTION_LOGON: # class attribute ACTION_LOGON holds the logon action value
146
+ verify()
147
+ p4cpm.close_success() # terminate with success state
148
+ elif p4cpm.args.action == Python4CPM.ACTION_CHANGE: # class attribute ACTION_CHANGE holds the password change action value
149
+ change()
150
+ p4cpm.close_success() # terminate with success state
151
+ elif p4cpm.args.action == Python4CPM.ACTION_PRERECONCILE: # class attribute ACTION_PRERECONCILE holds the pre-reconcile action value
152
+ verify(from_reconcile=True)
153
+ p4cpm.close_success() # terminate with success state
154
+ # Alternatively ->
155
+ ## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
156
+ ## p4cpm.close_fail() # let CPM know to check the logs
157
+ elif p4cpm.args.action == Python4CPM.ACTION_RECONCILE: # class attribute ACTION_RECONCILE holds the reconcile action value
158
+ change(from_reconcile=True)
159
+ p4cpm.close_success() # terminate with success state
160
+ # Alternatively ->
161
+ ## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
162
+ ## p4cpm.close_fail() # let CPM know to check the logs
163
+ else:
164
+ p4cpm.log_error(f"invalid action: '{action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
165
+ p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
166
+ except Exception as e:
167
+ p4cpm.log_error(f"{type(e).__name__}: {e}")
168
+ p4cpm.close_fail()
169
+ ```
170
+ (*) a more realistic examples can be found [here](https://github.com/gonatienza/python4cpm/blob/main/examples).
171
+
172
+ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
173
+ 1. Verify -> the sciprt will be executed once with the `p4cpm.args.action` as `Python4CPM.ACTION_VERIFY`.
174
+ 2. Change -> the sciprt will be executed twice, once with the action `p4cpm.args.action` as `Python4CPM.ACTION_LOGON` and once as `Python4CPM.ACTION_CHANGE`.
175
+ - If all actions are not terminated with `p4cpm.close_success()` the overall change will fail.
176
+ 3. Reconcile -> the sciprt will be executed twice, once with the `p4cpm.args.action` as `Python4CPM.ACTION_PRERECONCILE` and once as `Python4CPM.ACTION_RECONCILE`.
177
+ - If all actions are not terminated with `p4cpm.close_success()` the overall reconcile will fail.
178
+ 4. When `p4cpm.args.action` comes as `Python4CPM.ACTION_VERIFY`, `Python4CPM.ACTION_LOGON` or `Python4CPM.ACTION_PRERECONCILE`: `p4cpm.secrets.new_password.get()` will always return an empty string.
179
+ 5. If a logon account is not linked, `p4cpm.args.logon_username` and `p4cpm.secrets.logon_password.get()` will return an empty string.
180
+ 6. If a reconcile account is not linked, `p4cpm.args.reconcile_username` and `p4cpm.secrets.reconcile_password.get()` will return an empty string.
181
+
182
+
183
+ ### Installing dependancies in python venv
184
+
185
+ As with any python venv, you can install dependancies in your venv.
186
+ 1. If your CPM can connect to the internet:
187
+ - You can use regular pip install commands (e.g., `c:\venv\Scripts\pip.exe install requests`).
188
+ 2. If your CPM cannot connect to the internet:
189
+ - You can download packages for an offline install. More info [here](https://pip.pypa.io/en/stable/cli/pip_download/).
190
+
191
+
192
+ ## Dev Helper:
193
+
194
+ TPC is a binary Terminal Plugin Controller in CPM. It passes information to Python4CPM through arguments and prompts when calling the script.
195
+ For dev purposes, `TPCHelper` is a companion helper that simplifies the instantiation of the `Python4CPM` object by simulating how TPC passes those arguments and prompts.
196
+ Install this module (in a dev workstation) with:
197
+
198
+ ```bash
199
+ pip install https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-latest-py3-none-any.whl
200
+ ```
201
+
202
+ ### Example:
203
+
204
+ ```python
205
+ from python4cpm import TPCHelper, Python4CPM
206
+ from getpass import getpass
207
+
208
+ # Get secrets for your password, logon account password, reconcile account password and new password
209
+ # You can use an empty string if it does not apply
210
+ password = getpass("password: ") # password from account
211
+ logon_password = getpass("logon_password: ") # password from linked logon account
212
+ reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
213
+ new_password = getpass("new_password: ") # new password for the rotation
214
+
215
+ p4cpm = TPCHelper.run(
216
+ action=Python4CPM.ACTION_LOGON, # use actions from Python4CPM.ACTION_*
217
+ address="myapp.corp.local", # populate with the address from your account properties
218
+ username="jdoe", # populate with the username from your account properties
219
+ logon_username="ldoe", # populate with the logon account username from your linked logon account
220
+ reconcile_username="rdoe", # ppopulate with the reconcile account username from your linked logon account
221
+ logging="yes", # populate with the PythonLogging parameter from the platform: "yes" or "no"
222
+ logging_level="info", # populate with the PythonLoggingLevel parameter from the platform: "info" or "debug"
223
+ password=password,
224
+ logon_password=logon_password,
225
+ reconcile_password=reconcile_password,
226
+ new_password=new_password
227
+ )
228
+
229
+ # Use the p4cpm object during dev to build your script logic
230
+ assert password == p4cpm.secrets.password.get()
231
+ p4cpm.log_info("success!")
232
+ p4cpm.close_success()
233
+
234
+ # Remember for your final script:
235
+ ## changing the definition of p4cpm from TPCHelper.run() to Python4CPM("MyApp")
236
+ ## remove any secrets prompting
237
+ ## remove the TPCHelper import
238
+ ```
239
+
240
+ Remember for your final script:
241
+ - Change the definition of `p4cpm` from `p4cpm = TPCHelper.run(**kwargs)` to `p4cpm = Python4CPM("MyApp")`.
242
+ - Remove any secrets prompting or interactive interruptions.
243
+ - Remove the import of `TPCHelper`.
@@ -0,0 +1,209 @@
1
+ # Python4CPM
2
+
3
+ A simple way of using python scripts with CyberArk CPM rotations. This module levereages the [Terminal Plugin Controller](https://docs.cyberark.com/pam-self-hosted/latest/en/content/pasimp/plug-in-terminal-plugin-controller.htm) (TPC) in CPM to offload a password rotation logic into a script.
4
+
5
+ This platform allows you to duplicate it multiple times, simply changing its settings from Privilege Cloud/PVWA to point to different python scripts leveraging the module included with the base platform.
6
+
7
+ ## Installation
8
+
9
+ ### Preparing Python
10
+
11
+ 1. Install Python in CPM. **Python must be installed for all users when running the install wizard**.
12
+ 2. Create a venv in CPM, by running `py -m venv c:\venv`. Use the default location `c:\venv` or a custom one (e.g., `c:\my-venv-path`).
13
+ 3. Install `python4cpm` in your venv:
14
+ - If your CPM can connect to the internet, install with `c:\venv\Scripts\pip install python4cpm`.
15
+ - If your CPM cannot connect to the internet:
16
+ - Download the [latest wheel](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-wheel.zip).
17
+ - Copy the file to CPM, extract to a temporary location.
18
+ - From the temporary location run `c:\venv\Scripts\pip install --no-index --find-links=.\python4cpm-wheel python4cpm`.
19
+
20
+
21
+ ### Importing the platform
22
+
23
+ 1. Download the [latest platform zip file](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-platform.zip).
24
+ 2. Import the platform zip file into Privilege Cloud/PVWA `(Administration -> Platform Management -> Import platform)`.
25
+ 3. Craft your python script and place it within the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
26
+ 4. Duplicate the imported platform in Privilege Cloud/PVWA `(Administration -> Platform Management -> Application -> Python for CPM)` and name it after your application (e.g., My App).
27
+ 5. Edit the duplicated platform and specify the path of your placed script in the bin folder of CPM, under `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonScriptPath -> Value` (e.g., `bin\myapp.py`).
28
+ 6. If you used a custom venv location, also update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonExePath -> Value` with the custom path for the venv's `python.exe` file (e.g., `c:\my-venv-path\Scripts\python.exe`).
29
+ 7. If you want to disable logging, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging -> Value` to `no`.
30
+ 8. If you want to change the logging level to `debug`, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLoggingLevel -> Value` to `debug`.
31
+ 9. For new applications repeat steps from 3 to 8.
32
+
33
+
34
+ ## Python Script
35
+
36
+ ### Example:
37
+
38
+ ```python
39
+ from python4cpm import Python4CPM
40
+
41
+
42
+ p4cpm = Python4CPM("MyApp") # this instantiates the object and grabs all arguments and secrets shared by TPC
43
+
44
+ # These are the usable properties and related methods from the object:
45
+ p4cpm.args.action # action requested from CPM
46
+ p4cpm.args.address # address from the account address field
47
+ p4cpm.args.username # username from the account username field
48
+ p4cpm.args.reconcile_username # reconcile username from the linked reconcile account
49
+ p4cpm.args.logon_username # logon username from the linked logon account
50
+ p4cpm.args.logging # used to carry the platform logging settings for python
51
+ p4cpm.secrets.password.get() # get str from password received from the vault
52
+ p4cpm.secrets.new_password.get() # get str from new password in case of a rotation
53
+ p4cpm.secrets.logon_password.get() # get str from linked logon account password
54
+ p4cpm.secrets.reconcile_password.get() # get str from linked reconcile account password
55
+
56
+ # Logging methods -> Will only log if Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging is set to yes (default is yes)
57
+ p4cpm.log_error("this is an error message") # logs error into Logs/ThirdParty/Python4CPM/MyApp.log
58
+ p4cpm.log_warning("this is a warning message") # logs warning into Logs/ThirdParty/Python4CPM/MyApp.log
59
+ p4cpm.log_info("this is an info message") # logs info into Logs/ThirdParty/Python4CPM/MyApp.log
60
+ # Logging level -> Will only log debug messages if Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLoggingLevel is set to debug (default is info)
61
+ p4cpm.log_debug("this is an debug message") # logs info into Logs/ThirdParty/Python4CPM/MyApp.log if logging level is set to debug
62
+
63
+ # Terminate signals -> ALWAYS use one of the following three signals to terminate the script
64
+ ## p4cpm.close_success() # terminate with success state
65
+ ## p4cpm.close_fail() # terminate with recoverable failed state
66
+ ## p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
67
+ # If no signal is call, CPM will not know if the action was successful and display an error
68
+
69
+
70
+ # Verification example -> verify the username and password are valid
71
+ def verify(from_reconcile=False):
72
+ if from_reconcile is False:
73
+ pass
74
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.secrets.password.get()
75
+ # for your logic in a verification
76
+ else:
77
+ pass
78
+ # Use p4cpm.args.address, p4cpm.args.reconcile_username, p4cpm.secrets.reconcile_password.get()
79
+ # for your logic in a verification
80
+ result = True
81
+ if result is True:
82
+ p4cpm.log_info("verification successful") # logs info message into Logs/ThirdParty/Python4CPM/MyApp.log
83
+ else:
84
+ p4cpm.log_error("something went wrong") # logs error message Logs/ThirdParty/Python4CPM/MyApp.log
85
+ raise Exception("verify failed") # raise to trigger failed termination signal
86
+
87
+
88
+ # Rotation example -> rotate the password of the account
89
+ def change(from_reconcile=False):
90
+ if from_reconcile is False:
91
+ pass
92
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.secrets.password.get()
93
+ # and p4cpm.secrets.new_password.get() for your logic in a rotation
94
+ else:
95
+ pass
96
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.args.reconcile_username,
97
+ # p4cpm.secrets.reconcile_password.get() and p4cpm.secrets.new_password.get() for your logic in a reconciliation
98
+ result = True
99
+ if result is True:
100
+ p4cpm.log_info("rotation successful") # logs info message into Logs/ThirdParty/Python4CPM/MyApp.log
101
+ else:
102
+ p4cpm.log_error("something went wrong") # logs error message Logs/ThirdParty/Python4CPM/MyApp.log
103
+ raise Exception("change failed") # raise to trigger failed termination signal
104
+
105
+
106
+ if __name__ == "__main__":
107
+ try:
108
+ if action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
109
+ verify()
110
+ p4cpm.close_success() # terminate with success state
111
+ elif p4cpm.args.action == Python4CPM.ACTION_LOGON: # class attribute ACTION_LOGON holds the logon action value
112
+ verify()
113
+ p4cpm.close_success() # terminate with success state
114
+ elif p4cpm.args.action == Python4CPM.ACTION_CHANGE: # class attribute ACTION_CHANGE holds the password change action value
115
+ change()
116
+ p4cpm.close_success() # terminate with success state
117
+ elif p4cpm.args.action == Python4CPM.ACTION_PRERECONCILE: # class attribute ACTION_PRERECONCILE holds the pre-reconcile action value
118
+ verify(from_reconcile=True)
119
+ p4cpm.close_success() # terminate with success state
120
+ # Alternatively ->
121
+ ## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
122
+ ## p4cpm.close_fail() # let CPM know to check the logs
123
+ elif p4cpm.args.action == Python4CPM.ACTION_RECONCILE: # class attribute ACTION_RECONCILE holds the reconcile action value
124
+ change(from_reconcile=True)
125
+ p4cpm.close_success() # terminate with success state
126
+ # Alternatively ->
127
+ ## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
128
+ ## p4cpm.close_fail() # let CPM know to check the logs
129
+ else:
130
+ p4cpm.log_error(f"invalid action: '{action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
131
+ p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
132
+ except Exception as e:
133
+ p4cpm.log_error(f"{type(e).__name__}: {e}")
134
+ p4cpm.close_fail()
135
+ ```
136
+ (*) a more realistic examples can be found [here](https://github.com/gonatienza/python4cpm/blob/main/examples).
137
+
138
+ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
139
+ 1. Verify -> the sciprt will be executed once with the `p4cpm.args.action` as `Python4CPM.ACTION_VERIFY`.
140
+ 2. Change -> the sciprt will be executed twice, once with the action `p4cpm.args.action` as `Python4CPM.ACTION_LOGON` and once as `Python4CPM.ACTION_CHANGE`.
141
+ - If all actions are not terminated with `p4cpm.close_success()` the overall change will fail.
142
+ 3. Reconcile -> the sciprt will be executed twice, once with the `p4cpm.args.action` as `Python4CPM.ACTION_PRERECONCILE` and once as `Python4CPM.ACTION_RECONCILE`.
143
+ - If all actions are not terminated with `p4cpm.close_success()` the overall reconcile will fail.
144
+ 4. When `p4cpm.args.action` comes as `Python4CPM.ACTION_VERIFY`, `Python4CPM.ACTION_LOGON` or `Python4CPM.ACTION_PRERECONCILE`: `p4cpm.secrets.new_password.get()` will always return an empty string.
145
+ 5. If a logon account is not linked, `p4cpm.args.logon_username` and `p4cpm.secrets.logon_password.get()` will return an empty string.
146
+ 6. If a reconcile account is not linked, `p4cpm.args.reconcile_username` and `p4cpm.secrets.reconcile_password.get()` will return an empty string.
147
+
148
+
149
+ ### Installing dependancies in python venv
150
+
151
+ As with any python venv, you can install dependancies in your venv.
152
+ 1. If your CPM can connect to the internet:
153
+ - You can use regular pip install commands (e.g., `c:\venv\Scripts\pip.exe install requests`).
154
+ 2. If your CPM cannot connect to the internet:
155
+ - You can download packages for an offline install. More info [here](https://pip.pypa.io/en/stable/cli/pip_download/).
156
+
157
+
158
+ ## Dev Helper:
159
+
160
+ TPC is a binary Terminal Plugin Controller in CPM. It passes information to Python4CPM through arguments and prompts when calling the script.
161
+ For dev purposes, `TPCHelper` is a companion helper that simplifies the instantiation of the `Python4CPM` object by simulating how TPC passes those arguments and prompts.
162
+ Install this module (in a dev workstation) with:
163
+
164
+ ```bash
165
+ pip install https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-latest-py3-none-any.whl
166
+ ```
167
+
168
+ ### Example:
169
+
170
+ ```python
171
+ from python4cpm import TPCHelper, Python4CPM
172
+ from getpass import getpass
173
+
174
+ # Get secrets for your password, logon account password, reconcile account password and new password
175
+ # You can use an empty string if it does not apply
176
+ password = getpass("password: ") # password from account
177
+ logon_password = getpass("logon_password: ") # password from linked logon account
178
+ reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
179
+ new_password = getpass("new_password: ") # new password for the rotation
180
+
181
+ p4cpm = TPCHelper.run(
182
+ action=Python4CPM.ACTION_LOGON, # use actions from Python4CPM.ACTION_*
183
+ address="myapp.corp.local", # populate with the address from your account properties
184
+ username="jdoe", # populate with the username from your account properties
185
+ logon_username="ldoe", # populate with the logon account username from your linked logon account
186
+ reconcile_username="rdoe", # ppopulate with the reconcile account username from your linked logon account
187
+ logging="yes", # populate with the PythonLogging parameter from the platform: "yes" or "no"
188
+ logging_level="info", # populate with the PythonLoggingLevel parameter from the platform: "info" or "debug"
189
+ password=password,
190
+ logon_password=logon_password,
191
+ reconcile_password=reconcile_password,
192
+ new_password=new_password
193
+ )
194
+
195
+ # Use the p4cpm object during dev to build your script logic
196
+ assert password == p4cpm.secrets.password.get()
197
+ p4cpm.log_info("success!")
198
+ p4cpm.close_success()
199
+
200
+ # Remember for your final script:
201
+ ## changing the definition of p4cpm from TPCHelper.run() to Python4CPM("MyApp")
202
+ ## remove any secrets prompting
203
+ ## remove the TPCHelper import
204
+ ```
205
+
206
+ Remember for your final script:
207
+ - Change the definition of `p4cpm` from `p4cpm = TPCHelper.run(**kwargs)` to `p4cpm = Python4CPM("MyApp")`.
208
+ - Remove any secrets prompting or interactive interruptions.
209
+ - Remove the import of `TPCHelper`.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "python4cpm"
7
+ version = "1.0.10"
8
+ description = "Python for CPM"
9
+ authors = [
10
+ { name = "Gonzalo Atienza Rela", email = "gonatienza@gmail.com" }
11
+ ]
12
+ dependencies = []
13
+ requires-python = ">=3.8"
14
+ readme = { file = "README.md", content-type = "text/markdown" }
15
+ license = { file = "LICENSE" }
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["ruff"]
19
+
20
+ [tool.setuptools]
21
+ package-dir = { "" = "src" }
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.ruff.lint]
27
+ select = ["E", "F", "W", "S"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ from python4cpm.python4cpm import Args, Secret, Secrets, Python4CPM
2
+ from python4cpm.tpchelper import TPCHelper
3
+ from importlib.metadata import version as __version
4
+
5
+ __version__ = __version(__name__)
6
+
7
+ __all__ = [
8
+ Args,
9
+ Secret,
10
+ Secrets,
11
+ Python4CPM,
12
+ TPCHelper
13
+ ]
@@ -0,0 +1,257 @@
1
+ import os
2
+ import sys
3
+ import logging
4
+ from logging.handlers import RotatingFileHandler
5
+ from argparse import ArgumentParser
6
+
7
+
8
+ class Args:
9
+ ARGS = (
10
+ "action",
11
+ "address",
12
+ "username",
13
+ "logon_username",
14
+ "reconcile_username",
15
+ "logging",
16
+ "logging_level"
17
+ )
18
+
19
+ def __init__(
20
+ self: str,
21
+ action: str,
22
+ address: str,
23
+ username: str,
24
+ reconcile_username: str,
25
+ logon_username: str,
26
+ logging: str,
27
+ logging_level: str
28
+ ) -> None:
29
+ self._action = action
30
+ self._address = address
31
+ self._username = username
32
+ self._reconcile_username = reconcile_username
33
+ self._logon_username = logon_username
34
+ self._logging = logging
35
+ self._logging_level = logging_level
36
+
37
+ @property
38
+ def action(self) -> str:
39
+ return self._action
40
+
41
+ @property
42
+ def address(self) -> str:
43
+ return self._address
44
+
45
+ @property
46
+ def username(self) -> str:
47
+ return self._username
48
+
49
+ @property
50
+ def reconcile_username(self) -> str:
51
+ return self._reconcile_username
52
+
53
+ @property
54
+ def logon_username(self) -> str:
55
+ return self._logon_username
56
+
57
+ @property
58
+ def logging(self) -> str:
59
+ return self._logging
60
+
61
+ @property
62
+ def logging_level(self) -> str:
63
+ return self._logging_level
64
+
65
+
66
+ class Secret:
67
+ def __init__(self, value: str) -> None:
68
+ self._secret = value
69
+
70
+ def __repr__(self) -> str:
71
+ return f"{self.__class__.__name__}('***')"
72
+
73
+ def get(self) -> str:
74
+ return self._secret
75
+
76
+
77
+ class Secrets:
78
+ SECRETS = (
79
+ "password",
80
+ "logon_password",
81
+ "reconcile_password",
82
+ "new_password"
83
+ )
84
+
85
+ def __init__(
86
+ self: str,
87
+ password: str,
88
+ logon_password: str,
89
+ reconcile_password: str,
90
+ new_password: str
91
+ ) -> None:
92
+ self._password = Secret(password)
93
+ self._logon_password = Secret(logon_password)
94
+ self._reconcile_password = Secret(reconcile_password)
95
+ self._new_password = Secret(new_password)
96
+
97
+ @property
98
+ def password(self) -> str:
99
+ return self._password
100
+
101
+ @property
102
+ def new_password(self) -> str:
103
+ return self._new_password
104
+
105
+ @property
106
+ def logon_password(self) -> str:
107
+ return self._logon_password
108
+
109
+ @property
110
+ def reconcile_password(self) -> str:
111
+ return self._reconcile_password
112
+
113
+
114
+ class Python4CPM:
115
+ ACTION_VERIFY = "verifypass"
116
+ ACTION_LOGON = "logon"
117
+ ACTION_CHANGE = "changepass"
118
+ ACTION_PRERECONCILE = "prereconcilepass"
119
+ ACTION_RECONCILE = "reconcilepass"
120
+ _VALID_ACTIONS = (
121
+ ACTION_VERIFY,
122
+ ACTION_LOGON,
123
+ ACTION_CHANGE,
124
+ ACTION_PRERECONCILE,
125
+ ACTION_RECONCILE,
126
+ )
127
+ _LOGS_DIR = os.path.join("Logs", "ThirdParty", "Python4CPM")
128
+ _CPM_ROOT_DIR = "C:\\Program Files (x86)\\CyberArk\\Password Manager"
129
+ if os.path.exists(_CPM_ROOT_DIR):
130
+ _LOGS_DIR = os.path.join(_CPM_ROOT_DIR, _LOGS_DIR)
131
+ _LOGGING_ENABLED_VALUE = "yes"
132
+ _LOGGING_LEVELS = {
133
+ "info": logging.INFO,
134
+ "debug": logging.DEBUG
135
+ }
136
+ _SECRETS_PADDING = "@@@"
137
+ _SUCCESS_PROMPT = "~~~SUCCESS~~~"
138
+ _FAILED_RECOVERABLE_PROMPT = "~~~FAILED_RECOVERABLE~~~"
139
+ _FAILED_UNRECOVERABLE_PROMPT = "~~~FAILED_UNRECOVERABLE~~~"
140
+
141
+ def __init__(self, name: str) -> None:
142
+ self._name = name
143
+ args = self._get_args()
144
+ self._args = Args(**args)
145
+ self._logger = self._get_logger(self._name)
146
+ self.log_info("Python4CPM.__init__: initiating...")
147
+ self._log_args()
148
+ self._verify_action()
149
+ secrets = self._get_secrets()
150
+ self._secrets = Secrets(**secrets)
151
+
152
+ @property
153
+ def args(self) -> Args:
154
+ return self._args
155
+
156
+ @property
157
+ def secrets(self) -> Secrets:
158
+ return self._secrets
159
+
160
+ @property
161
+ def logger(self) -> logging.Logger:
162
+ return self._logger
163
+
164
+ def log_debug(self, message: str) -> None:
165
+ if self._logger is not None:
166
+ self._logger.debug(message)
167
+
168
+ def log_info(self, message: str) -> None:
169
+ if self._logger is not None:
170
+ self._logger.info(message)
171
+
172
+ def log_warning(self, message: str) -> None:
173
+ if self._logger is not None:
174
+ self._logger.warning(message)
175
+
176
+ def log_error(self, message: str) -> None:
177
+ if self._logger is not None:
178
+ self._logger.error(message)
179
+
180
+ @staticmethod
181
+ def _get_args() -> dict:
182
+ parser = ArgumentParser()
183
+ for arg in Args.ARGS:
184
+ parser.add_argument(f"--{arg}")
185
+ args = parser.parse_args()
186
+ return dict(vars(args))
187
+
188
+ def _get_secrets(self) -> dict:
189
+ secrets = {}
190
+ try:
191
+ for secret in Secrets.SECRETS:
192
+ prompt = self._SECRETS_PADDING + secret + self._SECRETS_PADDING
193
+ secrets[secret] = input(prompt)
194
+ common_message = f"Python4CPM._get_secrets: {secret} ->"
195
+ if secrets[secret]:
196
+ self.log_info(f"{common_message} [*******]")
197
+ else:
198
+ self.log_info(f"{common_message} [NOT SET]")
199
+ except Exception as e:
200
+ self.log_error(f"Python4CPM._get_secrets: {type(e).__name__}: {e}")
201
+ self.close_fail()
202
+ return secrets
203
+
204
+ def _verify_action(self) -> None:
205
+ if self.args.action not in self._VALID_ACTIONS:
206
+ self.log_warning(
207
+ f"Python4CPM._verify_action: unkonwn action -> {self.args.action}"
208
+ )
209
+
210
+ def _log_args(self) -> None:
211
+ for key, value in vars(self._args).items():
212
+ common_message = f"Python4CPM._log_args: {key.strip('_')} ->"
213
+ if value:
214
+ self.log_info(f"{common_message} {value}")
215
+ else:
216
+ self.log_info(f"{common_message} [NOT SET]")
217
+
218
+ def _get_logger(self, name: str) -> logging.Logger:
219
+ if self._args.logging is None:
220
+ return None
221
+ if self._args.logging.lower() != self._LOGGING_ENABLED_VALUE:
222
+ return None
223
+ os.makedirs(self._LOGS_DIR, exist_ok=True)
224
+ logs_file = os.path.join(self._LOGS_DIR, f"{name}.log")
225
+ logger = logging.getLogger(name)
226
+ logging_level = self._args.logging_level.lower()
227
+ if logging_level in self._LOGGING_LEVELS:
228
+ logger.setLevel(self._LOGGING_LEVELS[logging_level])
229
+ else:
230
+ logger.setLevel(self._LOGGING_LEVELS["info"])
231
+ handler = RotatingFileHandler(
232
+ filename=logs_file,
233
+ maxBytes=1 * 1024 * 1024,
234
+ backupCount=2
235
+ )
236
+ formatter = logging.Formatter(
237
+ fmt="%(asctime)s | %(levelname)s | %(message)s",
238
+ datefmt="%Y-%m-%d %H:%M:%S"
239
+ )
240
+ handler.setFormatter(formatter)
241
+ logger.addHandler(handler)
242
+ return logger
243
+
244
+ def close_fail(self, unrecoverable: bool = False) -> None:
245
+ if unrecoverable is False:
246
+ prompt = self._FAILED_RECOVERABLE_PROMPT
247
+ else:
248
+ prompt = self._FAILED_UNRECOVERABLE_PROMPT
249
+ self.log_error(f"Python4CPM.close_fail: closing with {prompt}")
250
+ print(prompt)
251
+ sys.exit(1)
252
+
253
+ def close_success(self) -> None:
254
+ prompt = self._SUCCESS_PROMPT
255
+ self.log_info(f"Python4CPM.close_success: closing with {prompt}")
256
+ print(prompt)
257
+ sys.exit(0)
@@ -0,0 +1,44 @@
1
+ from .python4cpm import Python4CPM, Args
2
+ from unittest import mock
3
+
4
+
5
+ class TPCHelper:
6
+ @classmethod
7
+ def run(
8
+ cls,
9
+ action: str = "",
10
+ address: str = "",
11
+ username: str = "",
12
+ logon_username: str = "",
13
+ reconcile_username: str = "",
14
+ logging: str = "",
15
+ logging_level: str = "",
16
+ password: str = "",
17
+ logon_password: str = "",
18
+ reconcile_password: str = "",
19
+ new_password: str = ""
20
+ ) -> Python4CPM:
21
+ args = [
22
+ "", # sys.argv[0] is ignored by argparse
23
+ f"--{Args.ARGS[0]}={action}",
24
+ f"--{Args.ARGS[1]}={address}",
25
+ f"--{Args.ARGS[2]}={username}",
26
+ f"--{Args.ARGS[3]}={logon_username}",
27
+ f"--{Args.ARGS[4]}={reconcile_username}",
28
+ f"--{Args.ARGS[5]}={logging}",
29
+ f"--{Args.ARGS[6]}={logging_level}"
30
+ ]
31
+ secrets = (
32
+ password,
33
+ logon_password,
34
+ reconcile_password,
35
+ new_password
36
+ )
37
+ with mock.patch(
38
+ "sys.argv",
39
+ args
40
+ ), mock.patch(
41
+ "builtins.input",
42
+ side_effect=secrets
43
+ ):
44
+ return Python4CPM(cls.__name__)
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.4
2
+ Name: python4cpm
3
+ Version: 1.0.10
4
+ Summary: Python for CPM
5
+ Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Gonzalo Atienza Rela
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in
18
+ all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
+ THE SOFTWARE.
27
+
28
+ Requires-Python: >=3.8
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Provides-Extra: dev
32
+ Requires-Dist: ruff; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Python4CPM
36
+
37
+ A simple way of using python scripts with CyberArk CPM rotations. This module levereages the [Terminal Plugin Controller](https://docs.cyberark.com/pam-self-hosted/latest/en/content/pasimp/plug-in-terminal-plugin-controller.htm) (TPC) in CPM to offload a password rotation logic into a script.
38
+
39
+ This platform allows you to duplicate it multiple times, simply changing its settings from Privilege Cloud/PVWA to point to different python scripts leveraging the module included with the base platform.
40
+
41
+ ## Installation
42
+
43
+ ### Preparing Python
44
+
45
+ 1. Install Python in CPM. **Python must be installed for all users when running the install wizard**.
46
+ 2. Create a venv in CPM, by running `py -m venv c:\venv`. Use the default location `c:\venv` or a custom one (e.g., `c:\my-venv-path`).
47
+ 3. Install `python4cpm` in your venv:
48
+ - If your CPM can connect to the internet, install with `c:\venv\Scripts\pip install python4cpm`.
49
+ - If your CPM cannot connect to the internet:
50
+ - Download the [latest wheel](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-wheel.zip).
51
+ - Copy the file to CPM, extract to a temporary location.
52
+ - From the temporary location run `c:\venv\Scripts\pip install --no-index --find-links=.\python4cpm-wheel python4cpm`.
53
+
54
+
55
+ ### Importing the platform
56
+
57
+ 1. Download the [latest platform zip file](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-platform.zip).
58
+ 2. Import the platform zip file into Privilege Cloud/PVWA `(Administration -> Platform Management -> Import platform)`.
59
+ 3. Craft your python script and place it within the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
60
+ 4. Duplicate the imported platform in Privilege Cloud/PVWA `(Administration -> Platform Management -> Application -> Python for CPM)` and name it after your application (e.g., My App).
61
+ 5. Edit the duplicated platform and specify the path of your placed script in the bin folder of CPM, under `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonScriptPath -> Value` (e.g., `bin\myapp.py`).
62
+ 6. If you used a custom venv location, also update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonExePath -> Value` with the custom path for the venv's `python.exe` file (e.g., `c:\my-venv-path\Scripts\python.exe`).
63
+ 7. If you want to disable logging, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging -> Value` to `no`.
64
+ 8. If you want to change the logging level to `debug`, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLoggingLevel -> Value` to `debug`.
65
+ 9. For new applications repeat steps from 3 to 8.
66
+
67
+
68
+ ## Python Script
69
+
70
+ ### Example:
71
+
72
+ ```python
73
+ from python4cpm import Python4CPM
74
+
75
+
76
+ p4cpm = Python4CPM("MyApp") # this instantiates the object and grabs all arguments and secrets shared by TPC
77
+
78
+ # These are the usable properties and related methods from the object:
79
+ p4cpm.args.action # action requested from CPM
80
+ p4cpm.args.address # address from the account address field
81
+ p4cpm.args.username # username from the account username field
82
+ p4cpm.args.reconcile_username # reconcile username from the linked reconcile account
83
+ p4cpm.args.logon_username # logon username from the linked logon account
84
+ p4cpm.args.logging # used to carry the platform logging settings for python
85
+ p4cpm.secrets.password.get() # get str from password received from the vault
86
+ p4cpm.secrets.new_password.get() # get str from new password in case of a rotation
87
+ p4cpm.secrets.logon_password.get() # get str from linked logon account password
88
+ p4cpm.secrets.reconcile_password.get() # get str from linked reconcile account password
89
+
90
+ # Logging methods -> Will only log if Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging is set to yes (default is yes)
91
+ p4cpm.log_error("this is an error message") # logs error into Logs/ThirdParty/Python4CPM/MyApp.log
92
+ p4cpm.log_warning("this is a warning message") # logs warning into Logs/ThirdParty/Python4CPM/MyApp.log
93
+ p4cpm.log_info("this is an info message") # logs info into Logs/ThirdParty/Python4CPM/MyApp.log
94
+ # Logging level -> Will only log debug messages if Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLoggingLevel is set to debug (default is info)
95
+ p4cpm.log_debug("this is an debug message") # logs info into Logs/ThirdParty/Python4CPM/MyApp.log if logging level is set to debug
96
+
97
+ # Terminate signals -> ALWAYS use one of the following three signals to terminate the script
98
+ ## p4cpm.close_success() # terminate with success state
99
+ ## p4cpm.close_fail() # terminate with recoverable failed state
100
+ ## p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
101
+ # If no signal is call, CPM will not know if the action was successful and display an error
102
+
103
+
104
+ # Verification example -> verify the username and password are valid
105
+ def verify(from_reconcile=False):
106
+ if from_reconcile is False:
107
+ pass
108
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.secrets.password.get()
109
+ # for your logic in a verification
110
+ else:
111
+ pass
112
+ # Use p4cpm.args.address, p4cpm.args.reconcile_username, p4cpm.secrets.reconcile_password.get()
113
+ # for your logic in a verification
114
+ result = True
115
+ if result is True:
116
+ p4cpm.log_info("verification successful") # logs info message into Logs/ThirdParty/Python4CPM/MyApp.log
117
+ else:
118
+ p4cpm.log_error("something went wrong") # logs error message Logs/ThirdParty/Python4CPM/MyApp.log
119
+ raise Exception("verify failed") # raise to trigger failed termination signal
120
+
121
+
122
+ # Rotation example -> rotate the password of the account
123
+ def change(from_reconcile=False):
124
+ if from_reconcile is False:
125
+ pass
126
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.secrets.password.get()
127
+ # and p4cpm.secrets.new_password.get() for your logic in a rotation
128
+ else:
129
+ pass
130
+ # Use p4cpm.args.address, p4cpm.args.username, p4cpm.args.reconcile_username,
131
+ # p4cpm.secrets.reconcile_password.get() and p4cpm.secrets.new_password.get() for your logic in a reconciliation
132
+ result = True
133
+ if result is True:
134
+ p4cpm.log_info("rotation successful") # logs info message into Logs/ThirdParty/Python4CPM/MyApp.log
135
+ else:
136
+ p4cpm.log_error("something went wrong") # logs error message Logs/ThirdParty/Python4CPM/MyApp.log
137
+ raise Exception("change failed") # raise to trigger failed termination signal
138
+
139
+
140
+ if __name__ == "__main__":
141
+ try:
142
+ if action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
143
+ verify()
144
+ p4cpm.close_success() # terminate with success state
145
+ elif p4cpm.args.action == Python4CPM.ACTION_LOGON: # class attribute ACTION_LOGON holds the logon action value
146
+ verify()
147
+ p4cpm.close_success() # terminate with success state
148
+ elif p4cpm.args.action == Python4CPM.ACTION_CHANGE: # class attribute ACTION_CHANGE holds the password change action value
149
+ change()
150
+ p4cpm.close_success() # terminate with success state
151
+ elif p4cpm.args.action == Python4CPM.ACTION_PRERECONCILE: # class attribute ACTION_PRERECONCILE holds the pre-reconcile action value
152
+ verify(from_reconcile=True)
153
+ p4cpm.close_success() # terminate with success state
154
+ # Alternatively ->
155
+ ## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
156
+ ## p4cpm.close_fail() # let CPM know to check the logs
157
+ elif p4cpm.args.action == Python4CPM.ACTION_RECONCILE: # class attribute ACTION_RECONCILE holds the reconcile action value
158
+ change(from_reconcile=True)
159
+ p4cpm.close_success() # terminate with success state
160
+ # Alternatively ->
161
+ ## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
162
+ ## p4cpm.close_fail() # let CPM know to check the logs
163
+ else:
164
+ p4cpm.log_error(f"invalid action: '{action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
165
+ p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
166
+ except Exception as e:
167
+ p4cpm.log_error(f"{type(e).__name__}: {e}")
168
+ p4cpm.close_fail()
169
+ ```
170
+ (*) a more realistic examples can be found [here](https://github.com/gonatienza/python4cpm/blob/main/examples).
171
+
172
+ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
173
+ 1. Verify -> the sciprt will be executed once with the `p4cpm.args.action` as `Python4CPM.ACTION_VERIFY`.
174
+ 2. Change -> the sciprt will be executed twice, once with the action `p4cpm.args.action` as `Python4CPM.ACTION_LOGON` and once as `Python4CPM.ACTION_CHANGE`.
175
+ - If all actions are not terminated with `p4cpm.close_success()` the overall change will fail.
176
+ 3. Reconcile -> the sciprt will be executed twice, once with the `p4cpm.args.action` as `Python4CPM.ACTION_PRERECONCILE` and once as `Python4CPM.ACTION_RECONCILE`.
177
+ - If all actions are not terminated with `p4cpm.close_success()` the overall reconcile will fail.
178
+ 4. When `p4cpm.args.action` comes as `Python4CPM.ACTION_VERIFY`, `Python4CPM.ACTION_LOGON` or `Python4CPM.ACTION_PRERECONCILE`: `p4cpm.secrets.new_password.get()` will always return an empty string.
179
+ 5. If a logon account is not linked, `p4cpm.args.logon_username` and `p4cpm.secrets.logon_password.get()` will return an empty string.
180
+ 6. If a reconcile account is not linked, `p4cpm.args.reconcile_username` and `p4cpm.secrets.reconcile_password.get()` will return an empty string.
181
+
182
+
183
+ ### Installing dependancies in python venv
184
+
185
+ As with any python venv, you can install dependancies in your venv.
186
+ 1. If your CPM can connect to the internet:
187
+ - You can use regular pip install commands (e.g., `c:\venv\Scripts\pip.exe install requests`).
188
+ 2. If your CPM cannot connect to the internet:
189
+ - You can download packages for an offline install. More info [here](https://pip.pypa.io/en/stable/cli/pip_download/).
190
+
191
+
192
+ ## Dev Helper:
193
+
194
+ TPC is a binary Terminal Plugin Controller in CPM. It passes information to Python4CPM through arguments and prompts when calling the script.
195
+ For dev purposes, `TPCHelper` is a companion helper that simplifies the instantiation of the `Python4CPM` object by simulating how TPC passes those arguments and prompts.
196
+ Install this module (in a dev workstation) with:
197
+
198
+ ```bash
199
+ pip install https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-latest-py3-none-any.whl
200
+ ```
201
+
202
+ ### Example:
203
+
204
+ ```python
205
+ from python4cpm import TPCHelper, Python4CPM
206
+ from getpass import getpass
207
+
208
+ # Get secrets for your password, logon account password, reconcile account password and new password
209
+ # You can use an empty string if it does not apply
210
+ password = getpass("password: ") # password from account
211
+ logon_password = getpass("logon_password: ") # password from linked logon account
212
+ reconcile_password = getpass("reconcile_password: ") # password from linked reconcile account
213
+ new_password = getpass("new_password: ") # new password for the rotation
214
+
215
+ p4cpm = TPCHelper.run(
216
+ action=Python4CPM.ACTION_LOGON, # use actions from Python4CPM.ACTION_*
217
+ address="myapp.corp.local", # populate with the address from your account properties
218
+ username="jdoe", # populate with the username from your account properties
219
+ logon_username="ldoe", # populate with the logon account username from your linked logon account
220
+ reconcile_username="rdoe", # ppopulate with the reconcile account username from your linked logon account
221
+ logging="yes", # populate with the PythonLogging parameter from the platform: "yes" or "no"
222
+ logging_level="info", # populate with the PythonLoggingLevel parameter from the platform: "info" or "debug"
223
+ password=password,
224
+ logon_password=logon_password,
225
+ reconcile_password=reconcile_password,
226
+ new_password=new_password
227
+ )
228
+
229
+ # Use the p4cpm object during dev to build your script logic
230
+ assert password == p4cpm.secrets.password.get()
231
+ p4cpm.log_info("success!")
232
+ p4cpm.close_success()
233
+
234
+ # Remember for your final script:
235
+ ## changing the definition of p4cpm from TPCHelper.run() to Python4CPM("MyApp")
236
+ ## remove any secrets prompting
237
+ ## remove the TPCHelper import
238
+ ```
239
+
240
+ Remember for your final script:
241
+ - Change the definition of `p4cpm` from `p4cpm = TPCHelper.run(**kwargs)` to `p4cpm = Python4CPM("MyApp")`.
242
+ - Remove any secrets prompting or interactive interruptions.
243
+ - Remove the import of `TPCHelper`.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/python4cpm/__init__.py
5
+ src/python4cpm/python4cpm.py
6
+ src/python4cpm/tpchelper.py
7
+ src/python4cpm.egg-info/PKG-INFO
8
+ src/python4cpm.egg-info/SOURCES.txt
9
+ src/python4cpm.egg-info/dependency_links.txt
10
+ src/python4cpm.egg-info/requires.txt
11
+ src/python4cpm.egg-info/top_level.txt
12
+ tests/tests.py
@@ -0,0 +1,3 @@
1
+
2
+ [dev]
3
+ ruff
@@ -0,0 +1,2 @@
1
+ plugin
2
+ python4cpm
@@ -0,0 +1,181 @@
1
+ from python4cpm import Python4CPM, Secrets, Args, TPCHelper
2
+ from configparser import ConfigParser
3
+ import json
4
+ import pytest
5
+ import os
6
+ import sys
7
+
8
+
9
+ def get_prompts_and_inputs():
10
+ file_dir = os.path.dirname(__file__)
11
+ inputs_path = os.path.join(file_dir, "inputs.json")
12
+ with open(inputs_path, "r") as f:
13
+ inputs = json.load(f)
14
+ root_dir = os.path.dirname(file_dir)
15
+ prompts_file = os.path.join(root_dir, "src", "plugin", "Python4CPMPrompts.ini")
16
+ _prompts = ConfigParser()
17
+ _prompts.read(prompts_file)
18
+ prompts = _prompts["conditions"]
19
+ return inputs, prompts
20
+
21
+
22
+ INPUTS, PROMPTS = get_prompts_and_inputs()
23
+ ARGS = [
24
+ "", # sys.argv[0] is ignored by argparse
25
+ f"--{Args.ARGS[1]}={INPUTS['address']}",
26
+ f"--{Args.ARGS[2]}={INPUTS['username']}",
27
+ f"--{Args.ARGS[3]}={INPUTS['logon_username']}",
28
+ f"--{Args.ARGS[4]}={INPUTS['reconcile_username']}"
29
+ ]
30
+ LOGGING = ["yes", "YES", "bad"]
31
+ LOGGING_LEVELS = ["info", "debug", "DEBUG", "bad"]
32
+ ARGS_PARAMS = [
33
+ (action, logging, logging_level)
34
+ for logging in LOGGING
35
+ for logging_level in LOGGING_LEVELS
36
+ for action in Python4CPM._VALID_ACTIONS
37
+ ]
38
+ INPUTS_WITHOUT_NEW_PASSWORD = [
39
+ INPUTS[Secrets.SECRETS[0]],
40
+ INPUTS[Secrets.SECRETS[1]],
41
+ INPUTS[Secrets.SECRETS[2]],
42
+ ""
43
+ ]
44
+ INPUTS_WITH_NEW_PASSWORD = [
45
+ INPUTS[Secrets.SECRETS[0]],
46
+ INPUTS[Secrets.SECRETS[1]],
47
+ INPUTS[Secrets.SECRETS[2]],
48
+ INPUTS[Secrets.SECRETS[3]]
49
+ ]
50
+ ACTIONS_WITH_NEW_PASSWORD = (
51
+ Python4CPM.ACTION_CHANGE,
52
+ Python4CPM.ACTION_RECONCILE
53
+ )
54
+ ACTIONS_WITHOUT_NEW_PASSWORD = (
55
+ Python4CPM.ACTION_VERIFY,
56
+ Python4CPM.ACTION_LOGON,
57
+ Python4CPM.ACTION_PRERECONCILE
58
+ )
59
+ INPUT_PROMPTS = [
60
+ PROMPTS["SetPasswordPrompt"],
61
+ PROMPTS["SetLogonPasswordPrompt"],
62
+ PROMPTS["SetReconcilePasswordPronmpt"],
63
+ PROMPTS["SetNewPasswordPrompt"]
64
+ ]
65
+ SUCCESS_PROMPT_FROM_INI = PROMPTS["SuccessPrompt"]
66
+ FAILED_RECOVERABLE_PROMPT_FROM_INI = PROMPTS["FailedRecoverablePrompt"]
67
+ FAILED_UNRECOVERABLE_PROMPT_FROM_INI = PROMPTS["FailedUnrecoverablePrompt"]
68
+ CLOSE_SIGNALS = [
69
+ Python4CPM._SUCCESS_PROMPT,
70
+ Python4CPM._FAILED_RECOVERABLE_PROMPT,
71
+ Python4CPM._FAILED_UNRECOVERABLE_PROMPT
72
+ ]
73
+
74
+
75
+ class InputHandler:
76
+ def __init__(self, inputs):
77
+ self.iter_inputs = iter(inputs)
78
+
79
+ def __call__(self, prompt):
80
+ print(prompt)
81
+ return next(self.iter_inputs)
82
+
83
+
84
+ @pytest.mark.parametrize("action,logging,logging_level", ARGS_PARAMS)
85
+ def test_main(action, logging, logging_level, monkeypatch):
86
+ args = ARGS + [
87
+ f"--action={action}",
88
+ f"--logging={logging}",
89
+ f"--logging_level={logging_level}"
90
+ ]
91
+ print(f"arguments -> {args}")
92
+ monkeypatch.setattr(sys, "argv", args)
93
+ if action in ACTIONS_WITHOUT_NEW_PASSWORD:
94
+ inputs = INPUTS_WITHOUT_NEW_PASSWORD
95
+ elif action in ACTIONS_WITH_NEW_PASSWORD:
96
+ inputs = INPUTS_WITH_NEW_PASSWORD
97
+ print(f"inputs -> {inputs}")
98
+ input_handler = InputHandler(inputs)
99
+ monkeypatch.setattr("builtins.input", input_handler)
100
+ p4cpm = Python4CPM("PyTest")
101
+ for k, v in vars(p4cpm.args).items():
102
+ print(f"{k} -> {v}")
103
+ for k, v in vars(p4cpm.secrets).items():
104
+ print(f"{k} -> {v}")
105
+ assert p4cpm.args.action == action # noqa: S101
106
+ assert p4cpm.args.address == INPUTS["address"] # noqa: S101
107
+ assert p4cpm.args.username == INPUTS["username"] # noqa: S101
108
+ assert p4cpm.args.logon_username == INPUTS["logon_username"] # noqa: S101
109
+ assert p4cpm.args.reconcile_username == INPUTS["reconcile_username"] # noqa: S101
110
+ assert p4cpm.args.logging == logging # noqa: S101
111
+ assert p4cpm.args.logging_level == logging_level # noqa: S101
112
+ assert p4cpm.secrets.password.get() == INPUTS["password"] # noqa: S101
113
+ assert p4cpm.secrets.logon_password.get() == INPUTS["logon_password"] # noqa: S101
114
+ assert p4cpm.secrets.reconcile_password.get() == INPUTS["reconcile_password"] # noqa: S101
115
+ assert p4cpm.secrets.new_password.get() == inputs[3] # noqa: S101
116
+ if logging.lower() in Python4CPM._LOGGING_ENABLED_VALUE:
117
+ assert p4cpm._logger # noqa: S101
118
+ if logging_level.lower() == LOGGING_LEVELS[1]:
119
+ assert p4cpm._logger.level == p4cpm._LOGGING_LEVELS[LOGGING_LEVELS[1]] # noqa: S101
120
+ else:
121
+ assert p4cpm._logger.level == p4cpm._LOGGING_LEVELS[LOGGING_LEVELS[0]] # noqa: S101
122
+ else:
123
+ assert p4cpm._logger is None # noqa: S101
124
+
125
+
126
+ @pytest.mark.parametrize("close", CLOSE_SIGNALS)
127
+ def test_prompts(close, monkeypatch, capsys):
128
+ args = ARGS + ["--action=verifypass", "--logging=no"]
129
+ monkeypatch.setattr(sys, "argv", args)
130
+ input_handler = InputHandler(INPUTS_WITH_NEW_PASSWORD)
131
+ monkeypatch.setattr("builtins.input", input_handler)
132
+ p4cpm = Python4CPM("PyTest")
133
+ if close == CLOSE_SIGNALS[0]:
134
+ with pytest.raises(SystemExit) as e:
135
+ p4cpm.close_success()
136
+ assert e.value.code == 0 # noqa: S101
137
+ close_prompt = SUCCESS_PROMPT_FROM_INI
138
+ elif close == CLOSE_SIGNALS[1]:
139
+ with pytest.raises(SystemExit) as e:
140
+ p4cpm.close_fail()
141
+ assert e.value.code == 1 # noqa: S101
142
+ close_prompt = FAILED_RECOVERABLE_PROMPT_FROM_INI
143
+ elif close == CLOSE_SIGNALS[2]:
144
+ with pytest.raises(SystemExit) as e:
145
+ p4cpm.close_fail(unrecoverable=True)
146
+ assert e.value.code == 1 # noqa: S101
147
+ close_prompt = FAILED_UNRECOVERABLE_PROMPT_FROM_INI
148
+ prompts = INPUT_PROMPTS + [close_prompt]
149
+ out = capsys.readouterr().out.splitlines()
150
+ assert out == prompts # noqa: S101
151
+
152
+
153
+ def test_tpc_helper():
154
+ action = Python4CPM.ACTION_VERIFY
155
+ logging = LOGGING[0]
156
+ logging_level = LOGGING_LEVELS[0]
157
+ p4cpm = TPCHelper.run(
158
+ action=action,
159
+ address=INPUTS["address"],
160
+ username=INPUTS["username"],
161
+ logon_username=INPUTS["logon_username"],
162
+ reconcile_username=INPUTS["reconcile_username"],
163
+ logging=logging,
164
+ logging_level=logging_level,
165
+ password=INPUTS["password"],
166
+ logon_password=INPUTS["logon_password"],
167
+ reconcile_password=INPUTS["reconcile_password"],
168
+ new_password=INPUTS["new_password"]
169
+ )
170
+ assert isinstance(p4cpm, Python4CPM) # noqa: S101
171
+ assert p4cpm.args.action == action # noqa: S101
172
+ assert p4cpm.args.address == INPUTS["address"] # noqa: S101
173
+ assert p4cpm.args.username == INPUTS["username"] # noqa: S101
174
+ assert p4cpm.args.logon_username == INPUTS["logon_username"] # noqa: S101
175
+ assert p4cpm.args.reconcile_username == INPUTS["reconcile_username"] # noqa: S101
176
+ assert p4cpm.args.logging == logging # noqa: S101
177
+ assert p4cpm.args.logging_level == logging_level # noqa: S101
178
+ assert p4cpm.secrets.password.get() == INPUTS["password"] # noqa: S101
179
+ assert p4cpm.secrets.logon_password.get() == INPUTS["logon_password"] # noqa: S101
180
+ assert p4cpm.secrets.reconcile_password.get() == INPUTS["reconcile_password"] # noqa: S101
181
+ assert p4cpm.secrets.new_password.get() == INPUTS["new_password"] # noqa: S101