Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions docs/cyberark-pam-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ Keeper Vault/
| PaloAltoNetworks, CiscoIOS, CiscoASA, JuniperJunos, F5BigIP, CheckPointGAIA | pamMachine | ssh | 22 |
| CyberArk (internal) | pamMachine | ssh | 22 |
| BusinessWebsite | login | — | — |
| (empty platformId) | pamMachine | ssh | 22 |
| (unknown platformId) | pamMachine | ssh | 22 |
| Custom/renamed platform resolved by CyberArk metadata or keyword matching | inferred PAM type | inferred | inferred |
| Empty/unknown platform with no metadata or keyword match | login | — | — |

For an unresolved platform, fields not already represented by the login's title,
login, password, URL, or CyberArk notes are preserved as text custom fields.
Nested account metadata is flattened using dotted field names. Raw secret-bearing
payload keys are excluded from custom fields.

### Field Mapping

Expand Down Expand Up @@ -170,6 +175,9 @@ pam project cyberark-import pvwa.company.com --dry-run --output import.json --in
# Filter specific safes
pam project cyberark-import pvwa.company.com --safes "Production,Staging" --exclude-safes "Archive*"

# Import into Nested Share Folders (folders, records, rotation, PAM config)
pam project cyberark-import pvwa.company.com --name "CyberArk Migration" --gateway "My Gateway" --nsf

# Extend existing project
pam project cyberark-import pvwa.company.com --config <pam-config-uid>

Expand Down Expand Up @@ -199,7 +207,8 @@ pam project cyberark-cleanup --name "CyberArk Migration" --dry-run
| `--name`, `-n` | Project name |
| `--config`, `-c` | Extend existing PAM config UID |
| `--gateway`, `-g` | Gateway name or UID |
| `--folder-mode` | flat, exact, ksm (default) |
| `--folder-mode` | flat, exact, ksm, safe (default: safe) |
| `--nsf` | Create project folders/records/PAM config in Nested Share Folders |
| `--safes` | Include only these safes (comma/glob) |
| `--exclude-safes` | Exclude safes (comma/glob) |
| `--list-safes` | List safes and exit |
Expand Down Expand Up @@ -273,5 +282,3 @@ Before building the import JSON, the importer warns about:
- Rate limit handling: automatic retry on HTTP 429 with exponential backoff
- Pagination cap: MAX_FETCH_RECORDS (50,000) prevents OOM attacks
---


8 changes: 8 additions & 0 deletions keepercommander/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ def main(from_package=False):
if from_package:
sys.excepthook = handle_exceptions

# Internal: background service mode for a frozen (PyInstaller) executable - see service_app.py.
is_frozen = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
if is_frozen:
from .service.core.service_app import SERVICE_MODE_FLAG, run_background_service
if len(sys.argv) > 1 and sys.argv[1] == SERVICE_MODE_FLAG:
run_background_service()
return

sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
opts, flags = parser.parse_known_args(sys.argv[1:])

Expand Down
7 changes: 5 additions & 2 deletions keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from .sync_down import sync_down
from .team import Team
from .ttk import TTK
from .sanitization import sanitize_protobuf_json

current_milli_time = lambda: int(round(time.time() * 1000))

Expand Down Expand Up @@ -881,7 +882,8 @@ def communicate_rest(params, request, endpoint, *, rs_type=None, payload_version
if request:
if logging.getLogger().level <= logging.DEBUG:
js = google.protobuf.json_format.MessageToJson(request)
logging.debug('>>> [RQ] %s: %s', endpoint, js)
sanitized_js = sanitize_protobuf_json(js)
logging.debug('>>> [RQ] %s: %s', endpoint, sanitized_js)
api_request_payload.payload = request.SerializeToString()
if isinstance(payload_version, int):
api_request_payload.apiVersion = payload_version
Expand All @@ -894,7 +896,8 @@ def communicate_rest(params, request, endpoint, *, rs_type=None, payload_version
proto_rs.ParseFromString(rs)
if logging.getLogger().level <= logging.DEBUG:
js = google.protobuf.json_format.MessageToJson(proto_rs)
logging.debug('>>> [RS] %s: %s', endpoint, js)
sanitized_js = sanitize_protobuf_json(js)
logging.debug('>>> [RS] %s: %s', endpoint, sanitized_js)
return proto_rs
else:
return rs
Expand Down
64 changes: 60 additions & 4 deletions keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2429,7 +2429,7 @@ def print_root_rotation_setting(params, is_verbose=False, format_type='table'):
for c in configurations: # type: vault.TypedRecord
if c.record_type in ('pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration',
'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration',
'pamGitHubConfiguration'):
'pamGitHubConfiguration', 'pamHashiCorpConfiguration'):
facade.record = c
folder_info = resolve_pam_config_folder_info(
params, facade, c.record_uid)
Expand Down Expand Up @@ -2489,7 +2489,7 @@ def print_root_rotation_setting(params, is_verbose=False, format_type='table'):

common_parser = argparse.ArgumentParser(add_help=False)
common_parser.add_argument('--environment', '-env', dest='config_type', action='store',
choices=['local', 'aws', 'azure', 'gcp', 'domain', 'oci', 'github'], help='PAM Configuration Type')
choices=['local', 'aws', 'azure', 'gcp', 'domain', 'oci', 'github', 'hashicorp'], help='PAM Configuration Type')
common_parser.add_argument('--title', '-t', dest='title', action='store', help='Title of the PAM Configuration')
common_parser.add_argument('--gateway', '-g', dest='gateway_uid', action='store', help='Gateway UID or Name')
common_parser.add_argument('--shared-folder', '-sf', dest='shared_folder_uid', action='store',
Expand Down Expand Up @@ -2555,12 +2555,23 @@ def print_root_rotation_setting(params, is_verbose=False, format_type='table'):
github_group.add_argument('--github-base-url', dest='github_base_url', action='store',
help='GitHub Base URL')

hashicorp_group = common_parser.add_argument_group('hashicorp', 'HashiCorp Vault configuration')
hashicorp_group.add_argument('--hashicorp-id', dest='hashicorp_id', action='store', help='HashiCorp Id')
hashicorp_group.add_argument('--vault-base-url', dest='vault_base_url', action='store',
help='Vault Base URL (e.g., https://vault.company.com:8200)')
hashicorp_group.add_argument('--vault-token', dest='vault_token', action='store',
help='Vault Token (optional; syncIdentity takes precedence)')
hashicorp_group.add_argument('--vault-namespace', dest='vault_namespace', action='store',
help='Vault Namespace (optional; leave blank for Community Edition)')
hashicorp_group.add_argument('--vault-mount-path', dest='vault_mount_path', action='store',
help='Vault KV Mount Path (optional; defaults to "secret")')

class PamConfigurationEditMixin(RecordEditMixin):
pam_record_types = None
PAM_CONFIG_RECORD_TYPES = frozenset({
'pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration',
'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration',
'pamGitHubConfiguration',
'pamGitHubConfiguration', 'pamHashiCorpConfiguration',
})
PAM_RESOURCE_RECORD_TYPES = frozenset({
'pamDatabase', 'pamDirectory', 'pamMachine', 'pamRemoteBrowser',
Expand Down Expand Up @@ -2859,6 +2870,22 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va
oci_region = kwargs.get('oci_region')
if oci_region:
extra_properties.append(f'text.regionOci={oci_region}')
elif record.record_type == 'pamHashiCorpConfiguration':
hashicorp_id = kwargs.get('hashicorp_id')
if hashicorp_id:
extra_properties.append(f'text.pamHashiCorpId={hashicorp_id}')
vault_base_url = kwargs.get('vault_base_url')
if vault_base_url:
extra_properties.append(f'text.pamHashiCorpVaultBaseUrl={vault_base_url}')
vault_token = kwargs.get('vault_token')
if vault_token:
extra_properties.append(f'secret.pamHashiCorpVaultToken={vault_token}')
vault_namespace = kwargs.get('vault_namespace')
if vault_namespace:
extra_properties.append(f'text.pamHashiCorpVaultNamespace={vault_namespace}')
vault_mount_path = kwargs.get('vault_mount_path')
if vault_mount_path:
extra_properties.append(f'text.pamHashiCorpVaultMountPath={vault_mount_path}')
if extra_properties:
self.assign_typed_fields(record, [RecordEditMixin.parse_field(x) for x in extra_properties])

Expand Down Expand Up @@ -2936,9 +2963,11 @@ def execute(self, params, **kwargs):
record_type = 'pamDomainConfiguration'
elif config_type == 'oci':
record_type = 'pamOciConfiguration'
elif config_type == 'hashicorp':
record_type = 'pamHashiCorpConfiguration'
else:
raise CommandError('pam-config-new', f'--environment {config_type} is not supported'
' - supported options: local, aws, azure, gcp, domain, oci, github')
' - supported options: local, aws, azure, gcp, domain, oci, github, hashicorp')

title = kwargs.get('title')
if not title:
Expand Down Expand Up @@ -3754,6 +3783,28 @@ def _is_rotation_allowed_by_enforcement(params):
return True


def ensure_gateway_management_allowed(params):
# type: (KeeperParams) -> bool
"""Per-user enforcement gate on the 'allow_pam_gateway' role enforcement
(confirmed via live account_summary payload). Prints an error and returns
False when the user's enterprise enforcement disallows Gateway management.
Shared by both PAMCreateGatewayCommand and PAMGatewayRemoveCommand (and
their legacy discoveryrotation_v1 counterparts) to avoid duplicating the
check at every call site.
"""
try:
from .workflow.helpers import is_pam_action_allowed_by_enforcement
except ImportError as _e:
logging.debug('workflow.helpers not available; skipping gateway enforcement check: %s', _e)
return True

if not is_pam_action_allowed_by_enforcement(params, 'allow_pam_gateway'):
print(f"{bcolors.FAIL}Gateway management is not allowed by your enterprise "
f"enforcement (allow_pam_gateway).{bcolors.ENDC}")
return False
return True


class PAMGatewayActionRotateCommand(Command):
parser = argparse.ArgumentParser(prog='pam action rotate')
parser.add_argument('--record-uid', '-r', dest='record_uid', action='store',
Expand Down Expand Up @@ -4393,6 +4444,9 @@ def get_parser(self):
return PAMGatewayRemoveCommand.dr_remove_controller_parser

def execute(self, params, **kwargs):
if not ensure_gateway_management_allowed(params):
return

gateway_name = kwargs.get('gateway')
gateways = gateway_helper.get_all_gateways(params)

Expand Down Expand Up @@ -4461,6 +4515,8 @@ def get_parser(self):
return PAMCreateGatewayCommand.dr_create_controller_parser

def execute(self, params, **kwargs):
if not ensure_gateway_management_allowed(params):
return

gateway_name = kwargs.get('gateway_name')
ksm_app = kwargs.get('ksm_app')
Expand Down
7 changes: 6 additions & 1 deletion keepercommander/commands/discoveryrotation_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from keeper_secrets_manager_core.utils import url_safe_str_to_bytes

from .base import Command, GroupCommand, user_choice, dump_report_data, report_output_parser, json_output_parser, field_to_title, FolderMixin
from .discoveryrotation import PAMLegacyCommand
from .discoveryrotation import PAMLegacyCommand, ensure_gateway_management_allowed
from .folder import FolderMoveCommand
from .ksm import KSMCommand
from .pam import gateway_helper, router_helper
Expand Down Expand Up @@ -1710,6 +1710,9 @@ def get_parser(self):
return PAMGatewayRemoveCommand.dr_remove_controller_parser

def execute(self, params, **kwargs):
if not ensure_gateway_management_allowed(params):
return

gateway_name = kwargs.get('gateway')
gateways = gateway_helper.get_all_gateways(params)

Expand Down Expand Up @@ -1745,6 +1748,8 @@ def get_parser(self):
return PAMCreateGatewayCommand.dr_create_controller_parser

def execute(self, params, **kwargs):
if not ensure_gateway_management_allowed(params):
return

gateway_name = kwargs.get('gateway_name')
ksm_app = kwargs.get('ksm_app')
Expand Down
Loading
Loading