python4cpm 1.1.1__tar.gz → 1.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python4cpm-1.1.1/src/python4cpm.egg-info → python4cpm-1.1.2}/PKG-INFO +28 -28
- {python4cpm-1.1.1 → python4cpm-1.1.2}/README.md +27 -27
- {python4cpm-1.1.1 → python4cpm-1.1.2}/pyproject.toml +1 -1
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm/accounts.py +11 -18
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm/args.py +7 -6
- python4cpm-1.1.2/src/python4cpm/envhandler.py +29 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm/logger.py +3 -3
- python4cpm-1.1.2/src/python4cpm/nethelper.py +60 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm/python4cpm.py +6 -31
- {python4cpm-1.1.1 → python4cpm-1.1.2/src/python4cpm.egg-info}/PKG-INFO +28 -28
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm.egg-info/SOURCES.txt +1 -0
- python4cpm-1.1.1/src/python4cpm/nethelper.py +0 -46
- {python4cpm-1.1.1 → python4cpm-1.1.2}/LICENSE +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/setup.cfg +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm/__init__.py +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm/crypto.py +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm/python4cpmhandler.py +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm/secret.py +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm.egg-info/dependency_links.txt +0 -0
- {python4cpm-1.1.1 → python4cpm-1.1.2}/src/python4cpm.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python4cpm
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: Python for CPM
|
|
5
5
|
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,9 +11,15 @@ Dynamic: license-file
|
|
|
11
11
|
|
|
12
12
|
# Python4CPM
|
|
13
13
|
|
|
14
|
-
A simple way of using python scripts with CyberArk CPM/SRS
|
|
14
|
+
A simple and secure way of using python scripts with CyberArk CPM/SRS password rotations.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
This module leverages the [Credential Management .NET SDK](https://docs.cyberark.com/privilege-cloud-standard/latest/en/content/pasimp/plug-in-netinvoker.htm) from CyberArk to securely offload a password rotation logic into Python.
|
|
19
|
+
|
|
20
|
+
All objects are collected from the SDK and sent as environment context to be picked up by the `python4cpm` module during the subprocess execution of python. All secrets of such environment are protected and encrypted by [Data Protection API (DPAPI)](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection), until they are explicitely retrieved in your python script runtime, invoking the `Secret.get()` method. Finally, python controls the termination signal sent back to the SDK, which is consequently used as the return code to CPM/SRS. Such as a successful or failed (recoverable or not) result of the requested action.
|
|
21
|
+
|
|
22
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (with regular day two operations from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
17
23
|
|
|
18
24
|
## Installation
|
|
19
25
|
|
|
@@ -98,9 +104,7 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
98
104
|
self.logger.info("this is an info message")
|
|
99
105
|
self.logger.debug("this is a debug message")
|
|
100
106
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
## The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
107
|
+
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
104
108
|
|
|
105
109
|
=============================
|
|
106
110
|
REQUIRED TERMINATION SIGNALS
|
|
@@ -108,13 +112,13 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
108
112
|
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
109
113
|
|
|
110
114
|
self.close_success()
|
|
111
|
-
# terminate with success state
|
|
115
|
+
# terminate and provide CPM/SRS with a success state
|
|
112
116
|
|
|
113
117
|
self.close_fail()
|
|
114
|
-
# terminate with
|
|
118
|
+
# terminate and provide CPM/SRS with a failed recoverable state
|
|
115
119
|
|
|
116
120
|
self.close_fail(unrecoverable=True)
|
|
117
|
-
# terminate with
|
|
121
|
+
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
118
122
|
|
|
119
123
|
When calling a signal sys.exit is invoked and the script is terminated.
|
|
120
124
|
If no signal is called, and the script finishes without any exception,
|
|
@@ -189,9 +193,10 @@ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
|
189
193
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
190
194
|
3. Reconcile -> the sciprt will be executed twice, running first the `MyRotator.prereconcile()` method and secondly the `MyRotator.reconcile()` method.
|
|
191
195
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
192
|
-
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.
|
|
193
|
-
5. If a logon account is not linked, `self.
|
|
194
|
-
6. If a reconcile account is not linked, `self.
|
|
196
|
+
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.target_account.new_password.get()` will always return an empty string.
|
|
197
|
+
5. If a logon account is not linked, `self.logon_account.username` and `self.logon_account.password.get()` will return empty strings.
|
|
198
|
+
6. If a reconcile account is not linked, `self.reconcile_account.username` and `self.reconcile_account.password.get()` will return empty strings.
|
|
199
|
+
7. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on the name of the subclass created (e.g., `MyRotator`).
|
|
195
200
|
|
|
196
201
|
|
|
197
202
|
### Installing dependencies in python venv
|
|
@@ -205,14 +210,9 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
205
210
|
|
|
206
211
|
## Dev Helper:
|
|
207
212
|
|
|
208
|
-
For dev purposes, `NETHelper` is a companion helper
|
|
209
|
-
Install this module (in a dev workstation) with:
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
pip install python4cpm
|
|
213
|
-
```
|
|
213
|
+
For dev purposes, `NETHelper` is a companion helper to test your scripts before shipping to CPM/SRS. It simplifies the instantiation of the `Python4CPM` or `Python4CPMHandler` objects by simulating how the plugin creates the environment context for the python module.
|
|
214
214
|
|
|
215
|
-
**Note**: As CPM
|
|
215
|
+
**Note**: As CPM and the SRS management agent run in Windows, the plugin was built to encrypt secrets using DPAPI (a windows only library). For dev purposes in Linux/Mac dev workstations, those secrets put in the environment context by `NETHelper` will be in plaintext. In windows dev workstations, `NETHelper` encrypts the secrets as the .NET plugin does. This is informational only, **the module will use its encryption/decryption capabilities automatically based on the platform** it is running on and you do not have to do anything specific to enable it.
|
|
216
216
|
|
|
217
217
|
### Example:
|
|
218
218
|
|
|
@@ -231,16 +231,16 @@ target_new_password = getpass("new_password: ") # new password for the rotation
|
|
|
231
231
|
|
|
232
232
|
NETHelper.set(
|
|
233
233
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
234
|
-
target_username="jdoe",
|
|
235
|
-
target_address="myapp.corp.local",
|
|
236
|
-
target_port="8443",
|
|
237
|
-
logon_username="ldoe",
|
|
238
|
-
reconcile_username="rdoe",
|
|
234
|
+
target_username="jdoe", # -> will fall under MyRotator.target_account.username
|
|
235
|
+
target_address="myapp.corp.local", # -> will fall under MyRotator.target_account.address
|
|
236
|
+
target_port="8443", # -> will fall under MyRotator.target_account.port
|
|
237
|
+
logon_username="ldoe", # -> will fall under MyRotator.logon_account.username
|
|
238
|
+
reconcile_username="rdoe", # -> will fall under MyRotator.reconcile_account.username
|
|
239
239
|
logging_level="debug", # "critical", "error", "warning", "info" or "debug"
|
|
240
|
-
target_password=target_password,
|
|
241
|
-
logon_password=logon_password,
|
|
242
|
-
reconcile_password=reconcile_password,
|
|
243
|
-
target_new_password=target_new_password
|
|
240
|
+
target_password=target_password, # -> will fall under MyRotator.target_account.password.get()
|
|
241
|
+
logon_password=logon_password, # -> will fall under MyRotator.logon_account.password.get()
|
|
242
|
+
reconcile_password=reconcile_password, # -> will fall under MyRotator.reconcile_account.password.get()
|
|
243
|
+
target_new_password=target_new_password # -> will fall under MyRotator.target_account.new_password.get()
|
|
244
244
|
)
|
|
245
245
|
|
|
246
246
|
class MyRotator(Python4CPMHandler):
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Python4CPM
|
|
2
2
|
|
|
3
|
-
A simple way of using python scripts with CyberArk CPM/SRS
|
|
3
|
+
A simple and secure way of using python scripts with CyberArk CPM/SRS password rotations.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
This module leverages the [Credential Management .NET SDK](https://docs.cyberark.com/privilege-cloud-standard/latest/en/content/pasimp/plug-in-netinvoker.htm) from CyberArk to securely offload a password rotation logic into Python.
|
|
8
|
+
|
|
9
|
+
All objects are collected from the SDK and sent as environment context to be picked up by the `python4cpm` module during the subprocess execution of python. All secrets of such environment are protected and encrypted by [Data Protection API (DPAPI)](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection), until they are explicitely retrieved in your python script runtime, invoking the `Secret.get()` method. Finally, python controls the termination signal sent back to the SDK, which is consequently used as the return code to CPM/SRS. Such as a successful or failed (recoverable or not) result of the requested action.
|
|
10
|
+
|
|
11
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (with regular day two operations from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
6
12
|
|
|
7
13
|
## Installation
|
|
8
14
|
|
|
@@ -87,9 +93,7 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
87
93
|
self.logger.info("this is an info message")
|
|
88
94
|
self.logger.debug("this is a debug message")
|
|
89
95
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
## The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
96
|
+
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
93
97
|
|
|
94
98
|
=============================
|
|
95
99
|
REQUIRED TERMINATION SIGNALS
|
|
@@ -97,13 +101,13 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
97
101
|
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
98
102
|
|
|
99
103
|
self.close_success()
|
|
100
|
-
# terminate with success state
|
|
104
|
+
# terminate and provide CPM/SRS with a success state
|
|
101
105
|
|
|
102
106
|
self.close_fail()
|
|
103
|
-
# terminate with
|
|
107
|
+
# terminate and provide CPM/SRS with a failed recoverable state
|
|
104
108
|
|
|
105
109
|
self.close_fail(unrecoverable=True)
|
|
106
|
-
# terminate with
|
|
110
|
+
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
107
111
|
|
|
108
112
|
When calling a signal sys.exit is invoked and the script is terminated.
|
|
109
113
|
If no signal is called, and the script finishes without any exception,
|
|
@@ -178,9 +182,10 @@ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
|
178
182
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
179
183
|
3. Reconcile -> the sciprt will be executed twice, running first the `MyRotator.prereconcile()` method and secondly the `MyRotator.reconcile()` method.
|
|
180
184
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
181
|
-
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.
|
|
182
|
-
5. If a logon account is not linked, `self.
|
|
183
|
-
6. If a reconcile account is not linked, `self.
|
|
185
|
+
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.target_account.new_password.get()` will always return an empty string.
|
|
186
|
+
5. If a logon account is not linked, `self.logon_account.username` and `self.logon_account.password.get()` will return empty strings.
|
|
187
|
+
6. If a reconcile account is not linked, `self.reconcile_account.username` and `self.reconcile_account.password.get()` will return empty strings.
|
|
188
|
+
7. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on the name of the subclass created (e.g., `MyRotator`).
|
|
184
189
|
|
|
185
190
|
|
|
186
191
|
### Installing dependencies in python venv
|
|
@@ -194,14 +199,9 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
194
199
|
|
|
195
200
|
## Dev Helper:
|
|
196
201
|
|
|
197
|
-
For dev purposes, `NETHelper` is a companion helper
|
|
198
|
-
Install this module (in a dev workstation) with:
|
|
199
|
-
|
|
200
|
-
```bash
|
|
201
|
-
pip install python4cpm
|
|
202
|
-
```
|
|
202
|
+
For dev purposes, `NETHelper` is a companion helper to test your scripts before shipping to CPM/SRS. It simplifies the instantiation of the `Python4CPM` or `Python4CPMHandler` objects by simulating how the plugin creates the environment context for the python module.
|
|
203
203
|
|
|
204
|
-
**Note**: As CPM
|
|
204
|
+
**Note**: As CPM and the SRS management agent run in Windows, the plugin was built to encrypt secrets using DPAPI (a windows only library). For dev purposes in Linux/Mac dev workstations, those secrets put in the environment context by `NETHelper` will be in plaintext. In windows dev workstations, `NETHelper` encrypts the secrets as the .NET plugin does. This is informational only, **the module will use its encryption/decryption capabilities automatically based on the platform** it is running on and you do not have to do anything specific to enable it.
|
|
205
205
|
|
|
206
206
|
### Example:
|
|
207
207
|
|
|
@@ -220,16 +220,16 @@ target_new_password = getpass("new_password: ") # new password for the rotation
|
|
|
220
220
|
|
|
221
221
|
NETHelper.set(
|
|
222
222
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
223
|
-
target_username="jdoe",
|
|
224
|
-
target_address="myapp.corp.local",
|
|
225
|
-
target_port="8443",
|
|
226
|
-
logon_username="ldoe",
|
|
227
|
-
reconcile_username="rdoe",
|
|
223
|
+
target_username="jdoe", # -> will fall under MyRotator.target_account.username
|
|
224
|
+
target_address="myapp.corp.local", # -> will fall under MyRotator.target_account.address
|
|
225
|
+
target_port="8443", # -> will fall under MyRotator.target_account.port
|
|
226
|
+
logon_username="ldoe", # -> will fall under MyRotator.logon_account.username
|
|
227
|
+
reconcile_username="rdoe", # -> will fall under MyRotator.reconcile_account.username
|
|
228
228
|
logging_level="debug", # "critical", "error", "warning", "info" or "debug"
|
|
229
|
-
target_password=target_password,
|
|
230
|
-
logon_password=logon_password,
|
|
231
|
-
reconcile_password=reconcile_password,
|
|
232
|
-
target_new_password=target_new_password
|
|
229
|
+
target_password=target_password, # -> will fall under MyRotator.target_account.password.get()
|
|
230
|
+
logon_password=logon_password, # -> will fall under MyRotator.logon_account.password.get()
|
|
231
|
+
reconcile_password=reconcile_password, # -> will fall under MyRotator.reconcile_account.password.get()
|
|
232
|
+
target_new_password=target_new_password # -> will fall under MyRotator.target_account.new_password.get()
|
|
233
233
|
)
|
|
234
234
|
|
|
235
235
|
class MyRotator(Python4CPMHandler):
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
from python4cpm.envhandler import EnvHandler, Props
|
|
1
2
|
from python4cpm.secret import Secret
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
class BaseAccount:
|
|
5
|
+
class BaseAccount(EnvHandler):
|
|
6
|
+
PROPS = Props("username", "password")
|
|
7
|
+
|
|
5
8
|
def __init__(
|
|
6
|
-
self
|
|
9
|
+
self,
|
|
7
10
|
username: str,
|
|
8
11
|
password: str
|
|
9
12
|
) -> None:
|
|
@@ -20,15 +23,11 @@ class BaseAccount:
|
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class TargetAccount(BaseAccount):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"target_address",
|
|
27
|
-
"target_port",
|
|
28
|
-
"target_new_password"
|
|
29
|
-
)
|
|
26
|
+
OBJ_PREFIX = "target_"
|
|
27
|
+
PROPS = Props("username", "password", "address", "port", "new_password")
|
|
28
|
+
|
|
30
29
|
def __init__(
|
|
31
|
-
self
|
|
30
|
+
self,
|
|
32
31
|
username: str,
|
|
33
32
|
password: str,
|
|
34
33
|
address: str,
|
|
@@ -57,14 +56,8 @@ class TargetAccount(BaseAccount):
|
|
|
57
56
|
|
|
58
57
|
|
|
59
58
|
class LogonAccount(BaseAccount):
|
|
60
|
-
|
|
61
|
-
"logon_username",
|
|
62
|
-
"logon_password"
|
|
63
|
-
)
|
|
59
|
+
OBJ_PREFIX = "logon_"
|
|
64
60
|
|
|
65
61
|
|
|
66
62
|
class ReconcileAccount(BaseAccount):
|
|
67
|
-
|
|
68
|
-
"reconcile_username",
|
|
69
|
-
"reconcile_password"
|
|
70
|
-
)
|
|
63
|
+
OBJ_PREFIX = "reconcile_"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
from python4cpm.envhandler import EnvHandler, Props
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Args(EnvHandler):
|
|
5
|
+
OBJ_PREFIX = "args_"
|
|
6
|
+
PROPS = Props("action", "logging_level")
|
|
6
7
|
|
|
7
8
|
def __init__(
|
|
8
|
-
self
|
|
9
|
+
self,
|
|
9
10
|
action: str,
|
|
10
11
|
logging_level: str
|
|
11
12
|
) -> None:
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Props:
|
|
5
|
+
def __init__(self, *props):
|
|
6
|
+
for prop in props:
|
|
7
|
+
setattr(self, prop, prop)
|
|
8
|
+
|
|
9
|
+
def __iter__(self) -> iter:
|
|
10
|
+
return iter(self.__dict__.values())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EnvHandler:
|
|
14
|
+
PREFIX = "python4cpm_"
|
|
15
|
+
OBJ_PREFIX = ""
|
|
16
|
+
PROPS = Props()
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def get_key(cls, key: str) -> str:
|
|
20
|
+
env_key = f"{cls.PREFIX}{cls.OBJ_PREFIX}{key}"
|
|
21
|
+
return env_key.upper()
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def get(cls) -> object:
|
|
25
|
+
kwargs = {}
|
|
26
|
+
for prop in cls.PROPS:
|
|
27
|
+
value = os.environ.get(cls.get_key(prop))
|
|
28
|
+
kwargs[prop] = value if value is not None else ""
|
|
29
|
+
return cls(**kwargs)
|
|
@@ -18,14 +18,14 @@ class Logger:
|
|
|
18
18
|
def get_logger(
|
|
19
19
|
cls,
|
|
20
20
|
name: str,
|
|
21
|
-
|
|
21
|
+
logging_level: str
|
|
22
22
|
) -> logging.Logger:
|
|
23
23
|
os.makedirs(cls._LOGS_DIR, exist_ok=True)
|
|
24
24
|
logs_file = os.path.join(cls._LOGS_DIR, f"{__name__}-{name}.log")
|
|
25
25
|
_id = os.urandom(4).hex()
|
|
26
26
|
logger = logging.getLogger(_id)
|
|
27
|
-
if
|
|
28
|
-
logger.setLevel(cls._LOGGING_LEVELS[
|
|
27
|
+
if logging_level.lower() in cls._LOGGING_LEVELS:
|
|
28
|
+
logger.setLevel(cls._LOGGING_LEVELS[logging_level.lower()])
|
|
29
29
|
else:
|
|
30
30
|
logger.setLevel(cls._DEFAULT_LEVEL)
|
|
31
31
|
handler = RotatingFileHandler(
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from python4cpm.python4cpm import Python4CPM
|
|
2
|
+
from python4cpm.args import Args
|
|
3
|
+
from python4cpm.accounts import TargetAccount, LogonAccount, ReconcileAccount
|
|
4
|
+
from python4cpm.crypto import Crypto
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NETHelper:
|
|
9
|
+
@classmethod
|
|
10
|
+
def set(
|
|
11
|
+
cls,
|
|
12
|
+
action: str = "",
|
|
13
|
+
logging_level: str = "",
|
|
14
|
+
target_username: str = "",
|
|
15
|
+
target_address: str = "",
|
|
16
|
+
target_port: str = "",
|
|
17
|
+
logon_username: str = "",
|
|
18
|
+
reconcile_username: str = "",
|
|
19
|
+
target_password: str = "",
|
|
20
|
+
logon_password: str = "",
|
|
21
|
+
reconcile_password: str = "",
|
|
22
|
+
target_new_password: str = ""
|
|
23
|
+
) -> None:
|
|
24
|
+
if Crypto.ENABLED:
|
|
25
|
+
target_password = Crypto.encrypt(target_password)
|
|
26
|
+
logon_password = Crypto.encrypt(logon_password)
|
|
27
|
+
reconcile_password = Crypto.encrypt(reconcile_password)
|
|
28
|
+
target_new_password = Crypto.encrypt(target_new_password)
|
|
29
|
+
keys = (
|
|
30
|
+
Args.get_key(Args.PROPS.action),
|
|
31
|
+
Args.get_key(Args.PROPS.logging_level),
|
|
32
|
+
TargetAccount.get_key(TargetAccount.PROPS.username),
|
|
33
|
+
TargetAccount.get_key(TargetAccount.PROPS.address),
|
|
34
|
+
TargetAccount.get_key(TargetAccount.PROPS.port),
|
|
35
|
+
LogonAccount.get_key(LogonAccount.PROPS.username),
|
|
36
|
+
ReconcileAccount.get_key(ReconcileAccount.PROPS.username),
|
|
37
|
+
TargetAccount.get_key(TargetAccount.PROPS.password),
|
|
38
|
+
LogonAccount.get_key(LogonAccount.PROPS.password),
|
|
39
|
+
ReconcileAccount.get_key(ReconcileAccount.PROPS.password),
|
|
40
|
+
TargetAccount.get_key(TargetAccount.PROPS.new_password)
|
|
41
|
+
)
|
|
42
|
+
values = (
|
|
43
|
+
action,
|
|
44
|
+
logging_level,
|
|
45
|
+
target_username,
|
|
46
|
+
target_address,
|
|
47
|
+
target_port,
|
|
48
|
+
logon_username,
|
|
49
|
+
reconcile_username,
|
|
50
|
+
target_password,
|
|
51
|
+
logon_password,
|
|
52
|
+
reconcile_password,
|
|
53
|
+
target_new_password
|
|
54
|
+
)
|
|
55
|
+
for i, key in enumerate(keys):
|
|
56
|
+
os.environ.update({key: values[i]})
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get(cls) -> Python4CPM:
|
|
60
|
+
return Python4CPM(cls.__name__)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import sys
|
|
3
2
|
import atexit
|
|
4
3
|
import logging
|
|
@@ -6,12 +5,8 @@ from python4cpm.secret import Secret
|
|
|
6
5
|
from python4cpm.args import Args
|
|
7
6
|
from python4cpm.crypto import Crypto
|
|
8
7
|
from python4cpm.logger import Logger
|
|
9
|
-
from python4cpm.accounts import
|
|
10
|
-
|
|
11
|
-
TargetAccount,
|
|
12
|
-
LogonAccount,
|
|
13
|
-
ReconcileAccount
|
|
14
|
-
)
|
|
8
|
+
from python4cpm.accounts import TargetAccount, LogonAccount, ReconcileAccount
|
|
9
|
+
|
|
15
10
|
|
|
16
11
|
class Python4CPM:
|
|
17
12
|
ACTION_VERIFY = "verifypass"
|
|
@@ -29,18 +24,17 @@ class Python4CPM:
|
|
|
29
24
|
_SUCCESS_CODE = 10
|
|
30
25
|
_FAILED_RECOVERABLE_CODE = 81
|
|
31
26
|
_FAILED_UNRECOVERABLE_CODE = 89
|
|
32
|
-
_ENV_PREFIX = "PYTHON4CPM_"
|
|
33
27
|
|
|
34
28
|
def __init__(self, name: str) -> None:
|
|
35
29
|
self._name = name
|
|
36
|
-
self._args =
|
|
30
|
+
self._args = Args.get()
|
|
31
|
+
self._target_account = TargetAccount.get()
|
|
32
|
+
self._logon_account = LogonAccount.get()
|
|
33
|
+
self._reconcile_account = ReconcileAccount.get()
|
|
37
34
|
self._logger = Logger.get_logger(self._name, self._args.logging_level)
|
|
38
35
|
self._logger.debug("Initiating...")
|
|
39
36
|
self._log_obj(self._args)
|
|
40
37
|
self._verify_action()
|
|
41
|
-
self._target_account = self._get_account(TargetAccount)
|
|
42
|
-
self._logon_account = self._get_account(LogonAccount)
|
|
43
|
-
self._reconcile_account = self._get_account(ReconcileAccount)
|
|
44
38
|
self._log_obj(self._target_account)
|
|
45
39
|
self._log_obj(self._logon_account)
|
|
46
40
|
self._log_obj(self._reconcile_account)
|
|
@@ -67,25 +61,6 @@ class Python4CPM:
|
|
|
67
61
|
def reconcile_account(self) -> ReconcileAccount:
|
|
68
62
|
return self._reconcile_account
|
|
69
63
|
|
|
70
|
-
@classmethod
|
|
71
|
-
def _get_env_key(cls, key: str) -> str:
|
|
72
|
-
return f"{cls._ENV_PREFIX}{key.upper()}"
|
|
73
|
-
|
|
74
|
-
@classmethod
|
|
75
|
-
def _get_args(cls) -> Args:
|
|
76
|
-
kwargs = {}
|
|
77
|
-
for kwarg in Args.ARGS:
|
|
78
|
-
_kwarg = os.environ.get(cls._get_env_key(kwarg))
|
|
79
|
-
kwargs[kwarg] = _kwarg if _kwarg is not None else ""
|
|
80
|
-
return Args(**kwargs)
|
|
81
|
-
|
|
82
|
-
def _get_account(self, account_class: BaseAccount) -> BaseAccount:
|
|
83
|
-
args = []
|
|
84
|
-
for arg in account_class.ENV_VARS:
|
|
85
|
-
_arg = os.environ.get(self._get_env_key(arg))
|
|
86
|
-
args.append(_arg if _arg is not None else "")
|
|
87
|
-
return account_class(*args)
|
|
88
|
-
|
|
89
64
|
def _verify_action(self) -> None:
|
|
90
65
|
if self._args.action not in self._VALID_ACTIONS:
|
|
91
66
|
self._logger.warning(f"Unkonwn action -> '{self._args.action}'")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python4cpm
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
4
4
|
Summary: Python for CPM
|
|
5
5
|
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,9 +11,15 @@ Dynamic: license-file
|
|
|
11
11
|
|
|
12
12
|
# Python4CPM
|
|
13
13
|
|
|
14
|
-
A simple way of using python scripts with CyberArk CPM/SRS
|
|
14
|
+
A simple and secure way of using python scripts with CyberArk CPM/SRS password rotations.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
This module leverages the [Credential Management .NET SDK](https://docs.cyberark.com/privilege-cloud-standard/latest/en/content/pasimp/plug-in-netinvoker.htm) from CyberArk to securely offload a password rotation logic into Python.
|
|
19
|
+
|
|
20
|
+
All objects are collected from the SDK and sent as environment context to be picked up by the `python4cpm` module during the subprocess execution of python. All secrets of such environment are protected and encrypted by [Data Protection API (DPAPI)](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection), until they are explicitely retrieved in your python script runtime, invoking the `Secret.get()` method. Finally, python controls the termination signal sent back to the SDK, which is consequently used as the return code to CPM/SRS. Such as a successful or failed (recoverable or not) result of the requested action.
|
|
21
|
+
|
|
22
|
+
This platform allows you to duplicate it multiple times, simply changing its settings (with regular day two operations from Privilege Cloud/PVWA) to point to different venvs and/or python scripts.
|
|
17
23
|
|
|
18
24
|
## Installation
|
|
19
25
|
|
|
@@ -98,9 +104,7 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
98
104
|
self.logger.info("this is an info message")
|
|
99
105
|
self.logger.debug("this is a debug message")
|
|
100
106
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
## The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
107
|
+
# The logging level comes from PythonLoggingLevel (platform parameters) (default is error)
|
|
104
108
|
|
|
105
109
|
=============================
|
|
106
110
|
REQUIRED TERMINATION SIGNALS
|
|
@@ -108,13 +112,13 @@ class MyRotator(Python4CPMHandler): # create a subclass for the Handler
|
|
|
108
112
|
Terminate signals -> MUST use one of the following three signals to terminate the script:
|
|
109
113
|
|
|
110
114
|
self.close_success()
|
|
111
|
-
# terminate with success state
|
|
115
|
+
# terminate and provide CPM/SRS with a success state
|
|
112
116
|
|
|
113
117
|
self.close_fail()
|
|
114
|
-
# terminate with
|
|
118
|
+
# terminate and provide CPM/SRS with a failed recoverable state
|
|
115
119
|
|
|
116
120
|
self.close_fail(unrecoverable=True)
|
|
117
|
-
# terminate with
|
|
121
|
+
# terminate and provide CPM/SRS with a failed unrecoverable state
|
|
118
122
|
|
|
119
123
|
When calling a signal sys.exit is invoked and the script is terminated.
|
|
120
124
|
If no signal is called, and the script finishes without any exception,
|
|
@@ -189,9 +193,10 @@ When doing `verify`, `change` or `reconcile` from Privilege Cloud/PVWA:
|
|
|
189
193
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
190
194
|
3. Reconcile -> the sciprt will be executed twice, running first the `MyRotator.prereconcile()` method and secondly the `MyRotator.reconcile()` method.
|
|
191
195
|
- If both actions are not terminated with `self.close_success()` and the scripts terminates without any exception, CPM/SRS will see this as a `self.close_fail(unrecoverable=True)`.
|
|
192
|
-
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.
|
|
193
|
-
5. If a logon account is not linked, `self.
|
|
194
|
-
6. If a reconcile account is not linked, `self.
|
|
196
|
+
4. When calling `MyRotator.verify()`, `MyRotator.logon()` or `MyRotator.prereconcile()`: `self.target_account.new_password.get()` will always return an empty string.
|
|
197
|
+
5. If a logon account is not linked, `self.logon_account.username` and `self.logon_account.password.get()` will return empty strings.
|
|
198
|
+
6. If a reconcile account is not linked, `self.reconcile_account.username` and `self.reconcile_account.password.get()` will return empty strings.
|
|
199
|
+
7. The python `Logger` places its logs in the `Logs/ThirdParty` directory. The filename will be based on the name of the subclass created (e.g., `MyRotator`).
|
|
195
200
|
|
|
196
201
|
|
|
197
202
|
### Installing dependencies in python venv
|
|
@@ -205,14 +210,9 @@ As with any python venv, you can install dependencies in your venv.
|
|
|
205
210
|
|
|
206
211
|
## Dev Helper:
|
|
207
212
|
|
|
208
|
-
For dev purposes, `NETHelper` is a companion helper
|
|
209
|
-
Install this module (in a dev workstation) with:
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
pip install python4cpm
|
|
213
|
-
```
|
|
213
|
+
For dev purposes, `NETHelper` is a companion helper to test your scripts before shipping to CPM/SRS. It simplifies the instantiation of the `Python4CPM` or `Python4CPMHandler` objects by simulating how the plugin creates the environment context for the python module.
|
|
214
214
|
|
|
215
|
-
**Note**: As CPM
|
|
215
|
+
**Note**: As CPM and the SRS management agent run in Windows, the plugin was built to encrypt secrets using DPAPI (a windows only library). For dev purposes in Linux/Mac dev workstations, those secrets put in the environment context by `NETHelper` will be in plaintext. In windows dev workstations, `NETHelper` encrypts the secrets as the .NET plugin does. This is informational only, **the module will use its encryption/decryption capabilities automatically based on the platform** it is running on and you do not have to do anything specific to enable it.
|
|
216
216
|
|
|
217
217
|
### Example:
|
|
218
218
|
|
|
@@ -231,16 +231,16 @@ target_new_password = getpass("new_password: ") # new password for the rotation
|
|
|
231
231
|
|
|
232
232
|
NETHelper.set(
|
|
233
233
|
action=Python4CPM.ACTION_CHANGE, # use actions from Python4CPM.ACTION_*
|
|
234
|
-
target_username="jdoe",
|
|
235
|
-
target_address="myapp.corp.local",
|
|
236
|
-
target_port="8443",
|
|
237
|
-
logon_username="ldoe",
|
|
238
|
-
reconcile_username="rdoe",
|
|
234
|
+
target_username="jdoe", # -> will fall under MyRotator.target_account.username
|
|
235
|
+
target_address="myapp.corp.local", # -> will fall under MyRotator.target_account.address
|
|
236
|
+
target_port="8443", # -> will fall under MyRotator.target_account.port
|
|
237
|
+
logon_username="ldoe", # -> will fall under MyRotator.logon_account.username
|
|
238
|
+
reconcile_username="rdoe", # -> will fall under MyRotator.reconcile_account.username
|
|
239
239
|
logging_level="debug", # "critical", "error", "warning", "info" or "debug"
|
|
240
|
-
target_password=target_password,
|
|
241
|
-
logon_password=logon_password,
|
|
242
|
-
reconcile_password=reconcile_password,
|
|
243
|
-
target_new_password=target_new_password
|
|
240
|
+
target_password=target_password, # -> will fall under MyRotator.target_account.password.get()
|
|
241
|
+
logon_password=logon_password, # -> will fall under MyRotator.logon_account.password.get()
|
|
242
|
+
reconcile_password=reconcile_password, # -> will fall under MyRotator.reconcile_account.password.get()
|
|
243
|
+
target_new_password=target_new_password # -> will fall under MyRotator.target_account.new_password.get()
|
|
244
244
|
)
|
|
245
245
|
|
|
246
246
|
class MyRotator(Python4CPMHandler):
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
from python4cpm.python4cpm import Python4CPM
|
|
2
|
-
from python4cpm.crypto import Crypto
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class NETHelper:
|
|
7
|
-
@classmethod
|
|
8
|
-
def set(
|
|
9
|
-
cls,
|
|
10
|
-
action: str = "",
|
|
11
|
-
logging_level: str = "",
|
|
12
|
-
target_username: str = "",
|
|
13
|
-
target_address: str = "",
|
|
14
|
-
target_port: str = "",
|
|
15
|
-
logon_username: str = "",
|
|
16
|
-
reconcile_username: str = "",
|
|
17
|
-
target_password: str = "",
|
|
18
|
-
logon_password: str = "",
|
|
19
|
-
reconcile_password: str = "",
|
|
20
|
-
target_new_password: str = ""
|
|
21
|
-
) -> None:
|
|
22
|
-
if Crypto.ENABLED:
|
|
23
|
-
target_password = Crypto.encrypt(target_password)
|
|
24
|
-
logon_password = Crypto.encrypt(logon_password)
|
|
25
|
-
reconcile_password = Crypto.encrypt(reconcile_password)
|
|
26
|
-
target_new_password = Crypto.encrypt(target_new_password)
|
|
27
|
-
env = {
|
|
28
|
-
"ACTION": action,
|
|
29
|
-
"LOGGING_LEVEL": logging_level,
|
|
30
|
-
"TARGET_USERNAME": target_username,
|
|
31
|
-
"TARGET_ADDRESS": target_address,
|
|
32
|
-
"TARGET_PORT": target_port,
|
|
33
|
-
"LOGON_USERNAME": logon_username,
|
|
34
|
-
"RECONCILE_USERNAME": reconcile_username,
|
|
35
|
-
"TARGET_PASSWORD": target_password,
|
|
36
|
-
"LOGON_PASSWORD": logon_password,
|
|
37
|
-
"RECONCILE_PASSWORD": reconcile_password,
|
|
38
|
-
"TARGET_NEW_PASSWORD": target_new_password
|
|
39
|
-
}
|
|
40
|
-
for key, value in env.items():
|
|
41
|
-
env_var = Python4CPM._ENV_PREFIX + key
|
|
42
|
-
os.environ[env_var] = value
|
|
43
|
-
|
|
44
|
-
@classmethod
|
|
45
|
-
def get(cls) -> Python4CPM:
|
|
46
|
-
return Python4CPM(cls.__name__)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|