python4cpm 1.0.14__tar.gz → 1.0.15__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.0.14/src/python4cpm.egg-info → python4cpm-1.0.15}/PKG-INFO +18 -16
- {python4cpm-1.0.14 → python4cpm-1.0.15}/README.md +17 -15
- {python4cpm-1.0.14 → python4cpm-1.0.15}/pyproject.toml +1 -1
- python4cpm-1.0.15/src/python4cpm/__init__.py +17 -0
- python4cpm-1.0.15/src/python4cpm/args.py +56 -0
- python4cpm-1.0.15/src/python4cpm/crypto.py +74 -0
- python4cpm-1.0.15/src/python4cpm/logger.py +46 -0
- python4cpm-1.0.15/src/python4cpm/nethelper.py +47 -0
- python4cpm-1.0.15/src/python4cpm/python4cpm.py +118 -0
- python4cpm-1.0.15/src/python4cpm/secrets.py +54 -0
- {python4cpm-1.0.14 → python4cpm-1.0.15/src/python4cpm.egg-info}/PKG-INFO +18 -16
- {python4cpm-1.0.14 → python4cpm-1.0.15}/src/python4cpm.egg-info/SOURCES.txt +5 -1
- python4cpm-1.0.14/src/python4cpm/__init__.py +0 -13
- python4cpm-1.0.14/src/python4cpm/python4cpm.py +0 -257
- python4cpm-1.0.14/src/python4cpm/tpchelper.py +0 -44
- {python4cpm-1.0.14 → python4cpm-1.0.15}/LICENSE +0 -0
- {python4cpm-1.0.14 → python4cpm-1.0.15}/setup.cfg +0 -0
- {python4cpm-1.0.14 → python4cpm-1.0.15}/src/python4cpm.egg-info/dependency_links.txt +0 -0
- {python4cpm-1.0.14 → python4cpm-1.0.15}/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.0.
|
|
3
|
+
Version: 1.0.15
|
|
4
4
|
Summary: Python for CPM
|
|
5
5
|
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -32,9 +32,9 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
# Python4CPM
|
|
34
34
|
|
|
35
|
-
A simple way of using python scripts with CyberArk CPM rotations. This module levereages the [
|
|
35
|
+
A simple way of using python scripts with CyberArk CPM rotations. This module levereages 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 a python script.
|
|
36
36
|
|
|
37
|
-
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
|
|
37
|
+
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 `python4cpm`.
|
|
38
38
|
|
|
39
39
|
## Installation
|
|
40
40
|
|
|
@@ -52,15 +52,16 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
52
52
|
|
|
53
53
|
### Importing the platform
|
|
54
54
|
|
|
55
|
-
1. Download the
|
|
56
|
-
2.
|
|
57
|
-
3.
|
|
58
|
-
4.
|
|
59
|
-
5.
|
|
60
|
-
6.
|
|
61
|
-
7. If you
|
|
62
|
-
8. If you want to
|
|
63
|
-
9.
|
|
55
|
+
1. Download the latest [Credential Management .NET SDK](https://community.cyberark.com/marketplace/s/#a3550000000EkA0AAK-a3950000000jjoOAAQ) and place its content in the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
|
|
56
|
+
2. Download the [latest platform zip file](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-platform.zip).
|
|
57
|
+
3. Import the platform zip file into Privilege Cloud/PVWA `(Administration -> Platform Management -> Import platform)`.
|
|
58
|
+
4. Craft your python script and place it within the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
|
|
59
|
+
5. 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).
|
|
60
|
+
6. 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`).
|
|
61
|
+
7. 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`).
|
|
62
|
+
8. If you want to disable logging, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging -> Value` to `no`.
|
|
63
|
+
9. 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`.
|
|
64
|
+
10. For new applications repeat steps from 4 to 9.
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
## Python Script
|
|
@@ -137,7 +138,7 @@ def change(from_reconcile=False):
|
|
|
137
138
|
|
|
138
139
|
if __name__ == "__main__":
|
|
139
140
|
try:
|
|
140
|
-
if action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
|
|
141
|
+
if p4cpm.args.action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
|
|
141
142
|
verify()
|
|
142
143
|
p4cpm.close_success() # terminate with success state
|
|
143
144
|
elif p4cpm.args.action == Python4CPM.ACTION_LOGON: # class attribute ACTION_LOGON holds the logon action value
|
|
@@ -159,7 +160,7 @@ if __name__ == "__main__":
|
|
|
159
160
|
## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
|
|
160
161
|
## p4cpm.close_fail() # let CPM know to check the logs
|
|
161
162
|
else:
|
|
162
|
-
p4cpm.log_error(f"invalid action: '{action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
163
|
+
p4cpm.log_error(f"invalid action: '{p4cpm.args.action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
163
164
|
p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
|
|
164
165
|
except Exception as e:
|
|
165
166
|
p4cpm.log_error(f"{type(e).__name__}: {e}")
|
|
@@ -189,14 +190,15 @@ As with any python venv, you can install dependancies in your venv.
|
|
|
189
190
|
|
|
190
191
|
## Dev Helper:
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
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.
|
|
193
|
+
For dev purposes, `NETHelper` is a companion helper that simplifies the instantiation of the `Python4CPM` object by simulating how the plugin passes arguments and secrets to `Python4CPM`.
|
|
194
194
|
Install this module (in a dev workstation) with:
|
|
195
195
|
|
|
196
196
|
```bash
|
|
197
197
|
pip install python4cpm
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
**Note**: As CPM runs in Windows, the plugin was built to pass secrets securely to the `Python4CPM.crypto` module using the Data Protection API (DPAPI). For dev purposes in Linux/Mac dev workstations, those secrets will appear as plaintext in the process environment. This is informational only, the module will use its encryption/decryption capabilities automatically in Windows and you do not have to do anything specific to enable it.
|
|
201
|
+
|
|
200
202
|
### Example:
|
|
201
203
|
|
|
202
204
|
```python
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Python4CPM
|
|
2
2
|
|
|
3
|
-
A simple way of using python scripts with CyberArk CPM rotations. This module levereages the [
|
|
3
|
+
A simple way of using python scripts with CyberArk CPM rotations. This module levereages 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 a python script.
|
|
4
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
|
|
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 `python4cpm`.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -20,15 +20,16 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
20
20
|
|
|
21
21
|
### Importing the platform
|
|
22
22
|
|
|
23
|
-
1. Download the
|
|
24
|
-
2.
|
|
25
|
-
3.
|
|
26
|
-
4.
|
|
27
|
-
5.
|
|
28
|
-
6.
|
|
29
|
-
7. If you
|
|
30
|
-
8. If you want to
|
|
31
|
-
9.
|
|
23
|
+
1. Download the latest [Credential Management .NET SDK](https://community.cyberark.com/marketplace/s/#a3550000000EkA0AAK-a3950000000jjoOAAQ) and place its content in the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
|
|
24
|
+
2. Download the [latest platform zip file](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-platform.zip).
|
|
25
|
+
3. Import the platform zip file into Privilege Cloud/PVWA `(Administration -> Platform Management -> Import platform)`.
|
|
26
|
+
4. Craft your python script and place it within the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
|
|
27
|
+
5. 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).
|
|
28
|
+
6. 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`).
|
|
29
|
+
7. 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`).
|
|
30
|
+
8. If you want to disable logging, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging -> Value` to `no`.
|
|
31
|
+
9. 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`.
|
|
32
|
+
10. For new applications repeat steps from 4 to 9.
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
## Python Script
|
|
@@ -105,7 +106,7 @@ def change(from_reconcile=False):
|
|
|
105
106
|
|
|
106
107
|
if __name__ == "__main__":
|
|
107
108
|
try:
|
|
108
|
-
if action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
|
|
109
|
+
if p4cpm.args.action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
|
|
109
110
|
verify()
|
|
110
111
|
p4cpm.close_success() # terminate with success state
|
|
111
112
|
elif p4cpm.args.action == Python4CPM.ACTION_LOGON: # class attribute ACTION_LOGON holds the logon action value
|
|
@@ -127,7 +128,7 @@ if __name__ == "__main__":
|
|
|
127
128
|
## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
|
|
128
129
|
## p4cpm.close_fail() # let CPM know to check the logs
|
|
129
130
|
else:
|
|
130
|
-
p4cpm.log_error(f"invalid action: '{action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
131
|
+
p4cpm.log_error(f"invalid action: '{p4cpm.args.action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
131
132
|
p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
|
|
132
133
|
except Exception as e:
|
|
133
134
|
p4cpm.log_error(f"{type(e).__name__}: {e}")
|
|
@@ -157,14 +158,15 @@ As with any python venv, you can install dependancies in your venv.
|
|
|
157
158
|
|
|
158
159
|
## Dev Helper:
|
|
159
160
|
|
|
160
|
-
|
|
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.
|
|
161
|
+
For dev purposes, `NETHelper` is a companion helper that simplifies the instantiation of the `Python4CPM` object by simulating how the plugin passes arguments and secrets to `Python4CPM`.
|
|
162
162
|
Install this module (in a dev workstation) with:
|
|
163
163
|
|
|
164
164
|
```bash
|
|
165
165
|
pip install python4cpm
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
**Note**: As CPM runs in Windows, the plugin was built to pass secrets securely to the `Python4CPM.crypto` module using the Data Protection API (DPAPI). For dev purposes in Linux/Mac dev workstations, those secrets will appear as plaintext in the process environment. This is informational only, the module will use its encryption/decryption capabilities automatically in Windows and you do not have to do anything specific to enable it.
|
|
169
|
+
|
|
168
170
|
### Example:
|
|
169
171
|
|
|
170
172
|
```python
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from python4cpm.python4cpm import Python4CPM
|
|
2
|
+
from python4cpm.args import Args
|
|
3
|
+
from python4cpm.secrets import SecureString, Secrets
|
|
4
|
+
from python4cpm.crypto import Crypto
|
|
5
|
+
from python4cpm.nethelper import NETHelper
|
|
6
|
+
from importlib.metadata import version as __version
|
|
7
|
+
|
|
8
|
+
__version__ = __version(__name__)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
Args,
|
|
12
|
+
SecureString,
|
|
13
|
+
Secrets,
|
|
14
|
+
Python4CPM,
|
|
15
|
+
Crypto,
|
|
16
|
+
NETHelper
|
|
17
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class Args:
|
|
2
|
+
ARGS = (
|
|
3
|
+
"action",
|
|
4
|
+
"address",
|
|
5
|
+
"username",
|
|
6
|
+
"logon_username",
|
|
7
|
+
"reconcile_username",
|
|
8
|
+
"logging",
|
|
9
|
+
"logging_level"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self: str,
|
|
14
|
+
action: str,
|
|
15
|
+
address: str,
|
|
16
|
+
username: str,
|
|
17
|
+
reconcile_username: str,
|
|
18
|
+
logon_username: str,
|
|
19
|
+
logging: str,
|
|
20
|
+
logging_level: str
|
|
21
|
+
) -> None:
|
|
22
|
+
self._action = action
|
|
23
|
+
self._address = address
|
|
24
|
+
self._username = username
|
|
25
|
+
self._reconcile_username = reconcile_username
|
|
26
|
+
self._logon_username = logon_username
|
|
27
|
+
self._logging = logging
|
|
28
|
+
self._logging_level = logging_level
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def action(self) -> str:
|
|
32
|
+
return self._action
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def address(self) -> str:
|
|
36
|
+
return self._address
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def username(self) -> str:
|
|
40
|
+
return self._username
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def reconcile_username(self) -> str:
|
|
44
|
+
return self._reconcile_username
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def logon_username(self) -> str:
|
|
48
|
+
return self._logon_username
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def logging(self) -> str:
|
|
52
|
+
return self._logging
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def logging_level(self) -> str:
|
|
56
|
+
return self._logging_level
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import ctypes
|
|
3
|
+
import ctypes.wintypes
|
|
4
|
+
import base64
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Crypto:
|
|
8
|
+
if sys.platform == "win32":
|
|
9
|
+
ENABLED = True
|
|
10
|
+
else:
|
|
11
|
+
ENABLED = False
|
|
12
|
+
|
|
13
|
+
class DataBlob(ctypes.Structure):
|
|
14
|
+
_fields_ = [
|
|
15
|
+
("cbData", ctypes.wintypes.DWORD),
|
|
16
|
+
("pbData", ctypes.POINTER(ctypes.c_ubyte))
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def _verify_enabled(cls):
|
|
21
|
+
if not cls.ENABLED:
|
|
22
|
+
raise OSError("DPAPI is only available on Windows")
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def decrypt(cls, base64_enc_string: str) -> str:
|
|
26
|
+
cls._verify_enabled()
|
|
27
|
+
encrypted_bytes = base64.b64decode(base64_enc_string.encode())
|
|
28
|
+
buffer = ctypes.create_string_buffer(encrypted_bytes)
|
|
29
|
+
input_blob = cls.DataBlob(
|
|
30
|
+
len(encrypted_bytes),
|
|
31
|
+
ctypes.cast(buffer, ctypes.POINTER(ctypes.c_ubyte))
|
|
32
|
+
)
|
|
33
|
+
output_blob = cls.DataBlob()
|
|
34
|
+
crypt_res = ctypes.windll.crypt32.CryptUnprotectData(
|
|
35
|
+
ctypes.byref(input_blob),
|
|
36
|
+
None,
|
|
37
|
+
None,
|
|
38
|
+
None,
|
|
39
|
+
None,
|
|
40
|
+
0,
|
|
41
|
+
ctypes.byref(output_blob)
|
|
42
|
+
)
|
|
43
|
+
if crypt_res:
|
|
44
|
+
plaintext = ctypes.string_at(output_blob.pbData, output_blob.cbData)
|
|
45
|
+
ctypes.windll.kernel32.LocalFree(output_blob.pbData)
|
|
46
|
+
return plaintext.decode()
|
|
47
|
+
else:
|
|
48
|
+
raise ctypes.WinError()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def encrypt(cls, plaintext: str) -> str:
|
|
52
|
+
cls._verify_enabled()
|
|
53
|
+
plain_bytes = plaintext.encode()
|
|
54
|
+
buffer = ctypes.create_string_buffer(plain_bytes)
|
|
55
|
+
input_blob = cls.DataBlob(
|
|
56
|
+
len(plain_bytes),
|
|
57
|
+
ctypes.cast(buffer, ctypes.POINTER(ctypes.c_ubyte))
|
|
58
|
+
)
|
|
59
|
+
output_blob = cls.DataBlob()
|
|
60
|
+
crypt_res = ctypes.windll.crypt32.CryptProtectData(
|
|
61
|
+
ctypes.byref(input_blob),
|
|
62
|
+
None,
|
|
63
|
+
None,
|
|
64
|
+
None,
|
|
65
|
+
None,
|
|
66
|
+
0,
|
|
67
|
+
ctypes.byref(output_blob)
|
|
68
|
+
)
|
|
69
|
+
if crypt_res:
|
|
70
|
+
encrypted = ctypes.string_at(output_blob.pbData, output_blob.cbData)
|
|
71
|
+
ctypes.windll.kernel32.LocalFree(output_blob.pbData)
|
|
72
|
+
return base64.b64encode(encrypted).decode()
|
|
73
|
+
else:
|
|
74
|
+
raise ctypes.WinError()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from logging.handlers import RotatingFileHandler
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_LOGS_DIR = os.path.join("Logs", "ThirdParty", "Python4CPM")
|
|
7
|
+
_CPM_ROOT_DIR = "C:\\Program Files (x86)\\CyberArk\\Password Manager"
|
|
8
|
+
if os.path.exists(_CPM_ROOT_DIR):
|
|
9
|
+
_LOGS_DIR = os.path.join(_CPM_ROOT_DIR, _LOGS_DIR)
|
|
10
|
+
_LOGGING_ENABLED_VALUE = "yes"
|
|
11
|
+
_LOGGING_LEVELS = {
|
|
12
|
+
"info": logging.INFO,
|
|
13
|
+
"debug": logging.DEBUG
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_logger(
|
|
18
|
+
name: str,
|
|
19
|
+
args_logging: str,
|
|
20
|
+
args_logging_level: str
|
|
21
|
+
) -> logging.Logger:
|
|
22
|
+
if args_logging is None:
|
|
23
|
+
return None
|
|
24
|
+
if args_logging.lower() != _LOGGING_ENABLED_VALUE:
|
|
25
|
+
return None
|
|
26
|
+
os.makedirs(_LOGS_DIR, exist_ok=True)
|
|
27
|
+
logs_file = os.path.join(_LOGS_DIR, f"{name}.log")
|
|
28
|
+
_id = os.urandom(4).hex()
|
|
29
|
+
logger = logging.getLogger(_id)
|
|
30
|
+
logging_level = args_logging_level.lower()
|
|
31
|
+
if logging_level in _LOGGING_LEVELS:
|
|
32
|
+
logger.setLevel(_LOGGING_LEVELS[logging_level])
|
|
33
|
+
else:
|
|
34
|
+
logger.setLevel(_LOGGING_LEVELS["info"])
|
|
35
|
+
handler = RotatingFileHandler(
|
|
36
|
+
filename=logs_file,
|
|
37
|
+
maxBytes=1 * 1024 * 1024,
|
|
38
|
+
backupCount=1
|
|
39
|
+
)
|
|
40
|
+
formatter = logging.Formatter(
|
|
41
|
+
fmt="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
|
|
42
|
+
datefmt="%Y-%m-%d %H:%M:%S"
|
|
43
|
+
)
|
|
44
|
+
handler.setFormatter(formatter)
|
|
45
|
+
logger.addHandler(handler)
|
|
46
|
+
return logger
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from python4cpm.python4cpm import Python4CPM
|
|
2
|
+
from python4cpm.args import Args
|
|
3
|
+
from python4cpm.secrets import Secrets
|
|
4
|
+
from python4cpm.crypto import Crypto
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NETHelper:
|
|
9
|
+
@classmethod
|
|
10
|
+
def run(
|
|
11
|
+
cls,
|
|
12
|
+
action: str = "",
|
|
13
|
+
address: str = "",
|
|
14
|
+
username: str = "",
|
|
15
|
+
logon_username: str = "",
|
|
16
|
+
reconcile_username: str = "",
|
|
17
|
+
logging: str = "",
|
|
18
|
+
logging_level: str = "",
|
|
19
|
+
password: str = "",
|
|
20
|
+
logon_password: str = "",
|
|
21
|
+
reconcile_password: str = "",
|
|
22
|
+
new_password: str = ""
|
|
23
|
+
) -> Python4CPM:
|
|
24
|
+
_args = [
|
|
25
|
+
action,
|
|
26
|
+
address,
|
|
27
|
+
username,
|
|
28
|
+
logon_username,
|
|
29
|
+
reconcile_username,
|
|
30
|
+
logging,
|
|
31
|
+
logging_level
|
|
32
|
+
]
|
|
33
|
+
for i, arg in enumerate(Args.ARGS):
|
|
34
|
+
os.environ[f"PYTHON4CPM_{arg.upper()}"] = _args[i]
|
|
35
|
+
_secrets = [
|
|
36
|
+
password,
|
|
37
|
+
logon_password,
|
|
38
|
+
reconcile_password,
|
|
39
|
+
new_password
|
|
40
|
+
]
|
|
41
|
+
for i, secret in enumerate(Secrets.SECRETS):
|
|
42
|
+
if Crypto.ENABLED:
|
|
43
|
+
_secret = Crypto.encrypt(_secrets[i])
|
|
44
|
+
else:
|
|
45
|
+
_secret = _secrets[i]
|
|
46
|
+
os.environ[f"PYTHON4CPM_{secret.upper()}"] = _secret
|
|
47
|
+
return Python4CPM(cls.__name__)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
from python4cpm.secrets import Secrets
|
|
5
|
+
from python4cpm.args import Args
|
|
6
|
+
from python4cpm.logger import get_logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Python4CPM:
|
|
10
|
+
ACTION_VERIFY = "verifypass"
|
|
11
|
+
ACTION_LOGON = "logon"
|
|
12
|
+
ACTION_CHANGE = "changepass"
|
|
13
|
+
ACTION_PRERECONCILE = "prereconcilepass"
|
|
14
|
+
ACTION_RECONCILE = "reconcilepass"
|
|
15
|
+
_VALID_ACTIONS = (
|
|
16
|
+
ACTION_VERIFY,
|
|
17
|
+
ACTION_LOGON,
|
|
18
|
+
ACTION_CHANGE,
|
|
19
|
+
ACTION_PRERECONCILE,
|
|
20
|
+
ACTION_RECONCILE,
|
|
21
|
+
)
|
|
22
|
+
_SUCCESS_CODE = 0
|
|
23
|
+
_FAILED_RECOVERABLE_CODE = 81
|
|
24
|
+
_FAILED_UNRECOVERABLE_CODE = 89
|
|
25
|
+
_ENV_PREFIX = "PYTHON4CPM_"
|
|
26
|
+
|
|
27
|
+
def __init__(self, name: str) -> None:
|
|
28
|
+
self._name = name
|
|
29
|
+
args = self._get_args()
|
|
30
|
+
self._args = Args(**args)
|
|
31
|
+
self._logger = get_logger(
|
|
32
|
+
self._name,
|
|
33
|
+
self._args.logging,
|
|
34
|
+
self._args.logging_level
|
|
35
|
+
)
|
|
36
|
+
self.log_info("Python4CPM.__init__: initiating...")
|
|
37
|
+
self._log_args()
|
|
38
|
+
self._verify_action()
|
|
39
|
+
secrets = self._get_secrets()
|
|
40
|
+
self._secrets = Secrets(**secrets)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def args(self) -> Args:
|
|
44
|
+
return self._args
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def secrets(self) -> Secrets:
|
|
48
|
+
return self._secrets
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def logger(self) -> logging.Logger:
|
|
52
|
+
return self._logger
|
|
53
|
+
|
|
54
|
+
def log_debug(self, message: str) -> None:
|
|
55
|
+
if self._logger is not None:
|
|
56
|
+
self._logger.debug(message)
|
|
57
|
+
|
|
58
|
+
def log_info(self, message: str) -> None:
|
|
59
|
+
if self._logger is not None:
|
|
60
|
+
self._logger.info(message)
|
|
61
|
+
|
|
62
|
+
def log_warning(self, message: str) -> None:
|
|
63
|
+
if self._logger is not None:
|
|
64
|
+
self._logger.warning(message)
|
|
65
|
+
|
|
66
|
+
def log_error(self, message: str) -> None:
|
|
67
|
+
if self._logger is not None:
|
|
68
|
+
self._logger.error(message)
|
|
69
|
+
|
|
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) -> dict:
|
|
76
|
+
args = {}
|
|
77
|
+
for arg in Args.ARGS:
|
|
78
|
+
args[arg] = os.environ.get(cls._get_env_key(arg))
|
|
79
|
+
return args
|
|
80
|
+
|
|
81
|
+
def _get_secrets(self) -> dict:
|
|
82
|
+
secrets = {}
|
|
83
|
+
for secret in Secrets.SECRETS:
|
|
84
|
+
secrets[secret] = os.environ.get(self._get_env_key(secret))
|
|
85
|
+
common_message = f"Python4CPM._get_secrets: {secret} ->"
|
|
86
|
+
if secrets[secret]:
|
|
87
|
+
self.log_info(f"{common_message} [*******]")
|
|
88
|
+
else:
|
|
89
|
+
self.log_info(f"{common_message} [NOT SET]")
|
|
90
|
+
return secrets
|
|
91
|
+
|
|
92
|
+
def _verify_action(self) -> None:
|
|
93
|
+
if self.args.action not in self._VALID_ACTIONS:
|
|
94
|
+
self.log_warning(
|
|
95
|
+
f"Python4CPM._verify_action: unkonwn action -> {self.args.action}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _log_args(self) -> None:
|
|
99
|
+
for key, value in vars(self._args).items():
|
|
100
|
+
common_message = f"Python4CPM._log_args: {key.strip('_')} ->"
|
|
101
|
+
if value:
|
|
102
|
+
self.log_info(f"{common_message} {value}")
|
|
103
|
+
else:
|
|
104
|
+
self.log_info(f"{common_message} [NOT SET]")
|
|
105
|
+
|
|
106
|
+
def close_fail(self, unrecoverable: bool = False) -> None:
|
|
107
|
+
if unrecoverable is False:
|
|
108
|
+
code = self._FAILED_RECOVERABLE_CODE
|
|
109
|
+
else:
|
|
110
|
+
code = self._FAILED_UNRECOVERABLE_CODE
|
|
111
|
+
self.log_error(f"Python4CPM.close_fail: closing with code {code}")
|
|
112
|
+
sys.exit(code)
|
|
113
|
+
|
|
114
|
+
def close_success(self) -> None:
|
|
115
|
+
self.log_info(
|
|
116
|
+
f"Python4CPM.close_success: closing with code {self._SUCCESS_CODE}"
|
|
117
|
+
)
|
|
118
|
+
sys.exit(self._SUCCESS_CODE)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from python4cpm.crypto import Crypto
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SecureString:
|
|
5
|
+
def __init__(self, secret: str) -> None:
|
|
6
|
+
self._secret = secret
|
|
7
|
+
self._is_encrypted = Crypto.ENABLED
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def is_encrypted(self):
|
|
11
|
+
return self._is_encrypted
|
|
12
|
+
|
|
13
|
+
def get(self) -> str:
|
|
14
|
+
if self._is_encrypted and self._secret:
|
|
15
|
+
return Crypto.decrypt(self._secret)
|
|
16
|
+
else:
|
|
17
|
+
return self._secret
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Secrets:
|
|
21
|
+
SECRETS = (
|
|
22
|
+
"password",
|
|
23
|
+
"logon_password",
|
|
24
|
+
"reconcile_password",
|
|
25
|
+
"new_password"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self: str,
|
|
30
|
+
password: str,
|
|
31
|
+
logon_password: str,
|
|
32
|
+
reconcile_password: str,
|
|
33
|
+
new_password: str
|
|
34
|
+
) -> None:
|
|
35
|
+
self._password = SecureString(password)
|
|
36
|
+
self._logon_password = SecureString(logon_password)
|
|
37
|
+
self._reconcile_password = SecureString(reconcile_password)
|
|
38
|
+
self._new_password = SecureString(new_password)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def password(self) -> str:
|
|
42
|
+
return self._password
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def new_password(self) -> str:
|
|
46
|
+
return self._new_password
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def logon_password(self) -> str:
|
|
50
|
+
return self._logon_password
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def reconcile_password(self) -> str:
|
|
54
|
+
return self._reconcile_password
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python4cpm
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.15
|
|
4
4
|
Summary: Python for CPM
|
|
5
5
|
Author-email: Gonzalo Atienza Rela <gonatienza@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -32,9 +32,9 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
# Python4CPM
|
|
34
34
|
|
|
35
|
-
A simple way of using python scripts with CyberArk CPM rotations. This module levereages the [
|
|
35
|
+
A simple way of using python scripts with CyberArk CPM rotations. This module levereages 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 a python script.
|
|
36
36
|
|
|
37
|
-
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
|
|
37
|
+
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 `python4cpm`.
|
|
38
38
|
|
|
39
39
|
## Installation
|
|
40
40
|
|
|
@@ -52,15 +52,16 @@ This platform allows you to duplicate it multiple times, simply changing its set
|
|
|
52
52
|
|
|
53
53
|
### Importing the platform
|
|
54
54
|
|
|
55
|
-
1. Download the
|
|
56
|
-
2.
|
|
57
|
-
3.
|
|
58
|
-
4.
|
|
59
|
-
5.
|
|
60
|
-
6.
|
|
61
|
-
7. If you
|
|
62
|
-
8. If you want to
|
|
63
|
-
9.
|
|
55
|
+
1. Download the latest [Credential Management .NET SDK](https://community.cyberark.com/marketplace/s/#a3550000000EkA0AAK-a3950000000jjoOAAQ) and place its content in the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
|
|
56
|
+
2. Download the [latest platform zip file](https://github.com/gonatienza/python4cpm/releases/download/latest/python4cpm-platform.zip).
|
|
57
|
+
3. Import the platform zip file into Privilege Cloud/PVWA `(Administration -> Platform Management -> Import platform)`.
|
|
58
|
+
4. Craft your python script and place it within the bin folder of CPM (`C:\Program Files (x86)\CyberArk\Password Manager\bin`).
|
|
59
|
+
5. 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).
|
|
60
|
+
6. 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`).
|
|
61
|
+
7. 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`).
|
|
62
|
+
8. If you want to disable logging, update `Target Account Platform -> Automatic Platform Management -> Additional Policy Settings -> Parameters -> PythonLogging -> Value` to `no`.
|
|
63
|
+
9. 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`.
|
|
64
|
+
10. For new applications repeat steps from 4 to 9.
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
## Python Script
|
|
@@ -137,7 +138,7 @@ def change(from_reconcile=False):
|
|
|
137
138
|
|
|
138
139
|
if __name__ == "__main__":
|
|
139
140
|
try:
|
|
140
|
-
if action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
|
|
141
|
+
if p4cpm.args.action == Python4CPM.ACTION_VERIFY: # class attribute ACTION_VERIFY holds the verify action value
|
|
141
142
|
verify()
|
|
142
143
|
p4cpm.close_success() # terminate with success state
|
|
143
144
|
elif p4cpm.args.action == Python4CPM.ACTION_LOGON: # class attribute ACTION_LOGON holds the logon action value
|
|
@@ -159,7 +160,7 @@ if __name__ == "__main__":
|
|
|
159
160
|
## p4cpm.log_error("reconciliation is not supported") # let the logs know that reconciliation is not supported
|
|
160
161
|
## p4cpm.close_fail() # let CPM know to check the logs
|
|
161
162
|
else:
|
|
162
|
-
p4cpm.log_error(f"invalid action: '{action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
163
|
+
p4cpm.log_error(f"invalid action: '{p4cpm.args.action}'") # logs into Logs/ThirdParty/Python4CPM/MyApp.log
|
|
163
164
|
p4cpm.close_fail(unrecoverable=True) # terminate with unrecoverable failed state
|
|
164
165
|
except Exception as e:
|
|
165
166
|
p4cpm.log_error(f"{type(e).__name__}: {e}")
|
|
@@ -189,14 +190,15 @@ As with any python venv, you can install dependancies in your venv.
|
|
|
189
190
|
|
|
190
191
|
## Dev Helper:
|
|
191
192
|
|
|
192
|
-
|
|
193
|
-
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.
|
|
193
|
+
For dev purposes, `NETHelper` is a companion helper that simplifies the instantiation of the `Python4CPM` object by simulating how the plugin passes arguments and secrets to `Python4CPM`.
|
|
194
194
|
Install this module (in a dev workstation) with:
|
|
195
195
|
|
|
196
196
|
```bash
|
|
197
197
|
pip install python4cpm
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
**Note**: As CPM runs in Windows, the plugin was built to pass secrets securely to the `Python4CPM.crypto` module using the Data Protection API (DPAPI). For dev purposes in Linux/Mac dev workstations, those secrets will appear as plaintext in the process environment. This is informational only, the module will use its encryption/decryption capabilities automatically in Windows and you do not have to do anything specific to enable it.
|
|
201
|
+
|
|
200
202
|
### Example:
|
|
201
203
|
|
|
202
204
|
```python
|
|
@@ -2,8 +2,12 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/python4cpm/__init__.py
|
|
5
|
+
src/python4cpm/args.py
|
|
6
|
+
src/python4cpm/crypto.py
|
|
7
|
+
src/python4cpm/logger.py
|
|
8
|
+
src/python4cpm/nethelper.py
|
|
5
9
|
src/python4cpm/python4cpm.py
|
|
6
|
-
src/python4cpm/
|
|
10
|
+
src/python4cpm/secrets.py
|
|
7
11
|
src/python4cpm.egg-info/PKG-INFO
|
|
8
12
|
src/python4cpm.egg-info/SOURCES.txt
|
|
9
13
|
src/python4cpm.egg-info/dependency_links.txt
|
|
@@ -1,13 +0,0 @@
|
|
|
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
|
-
]
|
|
@@ -1,257 +0,0 @@
|
|
|
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)
|
|
@@ -1,44 +0,0 @@
|
|
|
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__)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|