diff --git a/keepercommander/commands/discoveryrotation.py b/keepercommander/commands/discoveryrotation.py index 17fb6fe4d..221c96da5 100644 --- a/keepercommander/commands/discoveryrotation.py +++ b/keepercommander/commands/discoveryrotation.py @@ -17,8 +17,8 @@ import re import time from datetime import datetime -from typing import Dict, Optional, Any, Set, List from urllib.parse import urlparse, urlunparse +from typing import Optional, List import requests from keeper_secrets_manager_core.utils import url_safe_str_to_bytes @@ -77,6 +77,7 @@ from .pam_debug.link import PAMDebugLinkCommand from .pam_debug.rotation_setting import PAMDebugRotationSettingsCommand from .pam_debug.vertex import PAMDebugVertexCommand +from .pam.cnapp_commands import PAMCnappCommand from .pam_import.commands import PAMProjectCommand from keepercommander.commands.pam_cloud.pam_privileged_access import PAMPrivilegedAccessCommand from .pam_launch.launch import PAMLaunchCommand @@ -93,7 +94,6 @@ from .pam_saas.config import PAMActionSaasConfigCommand from .pam_saas.update import PAMActionSaasUpdateCommand from .tunnel_and_connections import PAMTunnelCommand, PAMConnectionCommand, PAMRbiCommand, PAMSplitCommand -from .pam.cnapp_commands import PAMCnappCommand from .universalsecretsync import ( PAMUniversalSyncConfigCommand, PAMUniversalSyncRunCommand @@ -288,10 +288,9 @@ def __init__(self): self.register_command('workflow', PAMWorkflowCommand(), 'Manage PAM Workflows', 'w') self.register_command('access', PAMPrivilegedAccessCommand(), 'Manage privileged cloud access operations', 'ac') - self.register_command('cnapp', PAMCnappCommand(), - 'Manage Cloud-Native Application Protection Platform integration', 'cn') self.register_command('universal-sync-config', PAMUniversalSyncConfigCommand(), 'Manage Universal Sync Configurations', 'usc') self.register_command('universal-sync-run', PAMUniversalSyncRunCommand(), 'Run Universal Sync', 'usr') + self.register_command('cnapp', PAMCnappCommand(), 'Manage CNAPP integrations', 'cn') class PAMGatewayCommand(GroupCommand): @@ -2340,7 +2339,8 @@ 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'): + 'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration', + 'pamGitHubConfiguration'): facade.record = c shared_folder_parents = find_parent_top_folder(params, c.record_uid) if shared_folder_parents: @@ -2402,7 +2402,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'], help='PAM Configuration Type') + choices=['local', 'aws', 'azure', 'gcp', 'domain', 'oci', 'github'], 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', @@ -2461,6 +2461,12 @@ def print_root_rotation_setting(params, is_verbose=False, format_type='table'): help='Google Workspace Administrator Email Address') gcp_group.add_argument('--gcp-region', dest='region_names', action='append', help='GCP Region Names') +github_group = common_parser.add_argument_group('github', 'GitHub configuration') +github_group.add_argument('--github-id', dest='github_id', action='store', help='GitHub Id') +github_group.add_argument('--personal-access-token', dest='personal_access_token', action='store', + help='Personal Access Token') +github_group.add_argument('--github-base-url', dest='github_base_url', action='store', + help='GitHub Base URL') class PamConfigurationEditMixin(RecordEditMixin): pam_record_types = None @@ -2642,6 +2648,16 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va if gcp_region: regions = '\n'.join(gcp_region) extra_properties.append(f'multiline.pamGcpRegionName={regions}') + elif record.record_type == 'pamGitHubConfiguration': + github_id = kwargs.get('github_id') + if github_id: + extra_properties.append(f'text.pamGitHubId={github_id}') + personal_access_token = kwargs.get('personal_access_token') + if personal_access_token: + extra_properties.append(f'secret.personalAccessToken={personal_access_token}') + github_base_url = kwargs.get('github_base_url') + if github_base_url: + extra_properties.append(f'text.pamGitHubBaseUrl={github_base_url}') elif record.record_type == 'pamAzureConfiguration': azure_id = kwargs.get('azure_id') if azure_id: @@ -2803,13 +2819,15 @@ def execute(self, params, **kwargs): record_type = 'pamNetworkConfiguration' elif config_type == 'gcp': record_type = 'pamGcpConfiguration' + elif config_type == 'github': + record_type = 'pamGitHubConfiguration' elif config_type == 'domain': record_type = 'pamDomainConfiguration' elif config_type == 'oci': record_type = 'pamOciConfiguration' else: raise CommandError('pam-config-new', f'--environment {config_type} is not supported' - ' - supported options: local, aws, azure, gcp, domain, oci') + ' - supported options: local, aws, azure, gcp, domain, oci, github') title = kwargs.get('title') if not title: @@ -2958,6 +2976,8 @@ def execute(self, params, **kwargs): record_type = 'pamNetworkConfiguration' elif config_type == 'gcp': record_type = 'pamGcpConfiguration' + elif config_type == 'github': + record_type = 'pamGitHubConfiguration' elif config_type == 'domain': record_type = 'pamDomainConfiguration' elif config_type == 'oci': diff --git a/unit-tests/test_command_enterprise_api_keys.py b/unit-tests/test_command_enterprise_api_keys.py index 92d4183f9..f43b0fe01 100644 --- a/unit-tests/test_command_enterprise_api_keys.py +++ b/unit-tests/test_command_enterprise_api_keys.py @@ -69,6 +69,9 @@ def test_api_key_list_json_format(self): self.assertEqual(len(TestEnterpriseApiKeys.expected_commands), 0) self.assertIsNotNone(result) + # Compute the expected expiration date for the active token (matches mock: now + 365 days) + token4_expiry = datetime.datetime.now() + datetime.timedelta(days=365) + token4_expiry_str = token4_expiry.strftime('%Y-%m-%d %H:%M:%S') # Assert that the JSON result matches the expected values for all entries expected_json = [ { @@ -100,7 +103,7 @@ def test_api_key_list_json_format(self): "name": "SIEM Tool", "status": "Active", "issued_date": "2025-07-08 14:16:07", - "expiration_date": "2030-07-08 14:16:07", + "expiration_date": token4_expiry_str, "integration": "SIEM:2" }, { @@ -661,13 +664,14 @@ def communicate_rest_success(params, request, path, rs_type=None): integration5.apiIntegrationTypeName = "SIEM" integration5.actionType = 2 - # Token 53 - Active + # Token 53 - Active (expiration is always 1 year from now to keep tests passing over time) token4 = rs.tokens.add() token4.token = "active_token_53" token4.name = "SIEM Tool" token4.enterprise_id = 8560 token4.issuedDate = int(datetime.datetime(2025, 7, 8, 14, 16, 7).timestamp() * 1000) - token4.expirationDate = int(datetime.datetime(2030, 7, 8, 14, 16, 7).timestamp() * 1000) + token4_expiry = datetime.datetime.now() + datetime.timedelta(days=365) + token4.expirationDate = int(token4_expiry.timestamp() * 1000) integration7 = token4.integrations.add() integration7.roleName = "SIEM" integration7.apiIntegrationTypeName = "SIEM"