Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
bad99e3
Generate command added
ukumar-ks Nov 12, 2025
43c5a10
Fix asyncio event loop cleanup with SSO auth (#92)
m-k8s Nov 13, 2025
1bede62
push notification cleanup
sk-keeper Nov 14, 2025
b587b88
team approve command added
adeshmukh-ks Nov 14, 2025
2883938
reset password command added
adeshmukh-ks Nov 14, 2025
af5e6ac
verify-records and verify-shared-folder commands added
ukumar-ks Nov 19, 2025
b3dcf43
Bug fixes
adeshmukh-ks Nov 19, 2025
391d2b3
KeeperParam and Proto update changes
adeshmukh-ks Nov 20, 2025
19b9d4b
Merge pull request #98 from Keeper-Security/verfiy-records
craiglurey Nov 20, 2025
a0312dd
Merge pull request #99 from Keeper-Security/bug-fixes
craiglurey Nov 20, 2025
065f70b
Added persistent-login example
adeshmukh-ks Dec 1, 2025
b2a48fc
changes
adeshmukh-ks Dec 1, 2025
0e2c1d1
Python SDK bugfixes
ukumar-ks Dec 8, 2025
445cd4a
Sdk examples Implemented
ukumar-ks Dec 9, 2025
59ea14e
Record and Folder Share Functionality added to SDK
adeshmukh-ks Dec 10, 2025
d50011f
Corrected constant strings
adeshmukh-ks Dec 11, 2025
72aceb3
Bug Fix and User-Role and Team-Role correlations added
ukumar-ks Dec 16, 2025
a6ccdeb
Ksm command migration
adeshmukh-ks Dec 18, 2025
f961a5b
User report command added
ukumar-ks Dec 18, 2025
29e7399
Bugfix SDK - 232 and 233
ukumar-ks Dec 22, 2025
b4dc93f
Pam gateway commands added
adeshmukh-ks Dec 30, 2025
8899ed0
Security audit report command added
ukumar-ks Jan 5, 2026
85f3e79
KeeperPAM Config Commands Added
adeshmukh-ks Jan 7, 2026
789ae09
Bug Fixes
adeshmukh-ks Jan 7, 2026
f4120d8
share-report and share-records-report command added
adeshmukh-ks Jan 9, 2026
997a088
PEDM: approval extend
sk-keeper Dec 26, 2025
65a5f0e
Recover from invalid device error
sk-keeper Jan 11, 2026
381ff35
KEPM: expiration parser
sk-keeper Jan 11, 2026
db47ffc
Add "certificate_check" property
sk-keeper Jan 11, 2026
a93e841
KEPM: last seen
sk-keeper Jan 11, 2026
861255b
Aging report and action-report commands added
ukumar-ks Jan 15, 2026
3159583
Merge remote-tracking branch 'origin/release' into pam-config-int
adeshmukh-ks Jan 16, 2026
66df9c6
refactoring changes
adeshmukh-ks Jan 16, 2026
d5e98d1
move command fix
sk-keeper Jan 16, 2026
bf24e7e
refactoring changes
adeshmukh-ks Jan 16, 2026
282256f
2fa commands added
adeshmukh-ks Jan 19, 2026
809d58f
Merge remote-tracking branch 'origin/release' into pam-config-int
adeshmukh-ks Jan 21, 2026
867d77a
enterprise-push command added
adeshmukh-ks Feb 16, 2026
55af2b3
Compliance commands added
adeshmukh-ks Feb 20, 2026
f1ff42e
breachwatch report command added
adeshmukh-ks Feb 20, 2026
87df931
Merge remote-tracking branch 'origin/release' into pam-config-int
adeshmukh-ks Feb 24, 2026
407011d
PAM commands
adeshmukh-ks Mar 25, 2026
3b97aa7
Merge remote-tracking branch 'origin/release' into pam-config-int
adeshmukh-ks Mar 30, 2026
3505aa9
KeeperParam and Proto update changes
adeshmukh-ks Nov 20, 2025
5cb82f9
temp
adeshmukh-ks Mar 31, 2026
09e3a71
import fix
adeshmukh-ks Mar 31, 2026
768a48e
fixed circular import and definition errors
adeshmukh-ks Mar 31, 2026
335dc6e
import error fix
adeshmukh-ks Mar 31, 2026
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
1 change: 0 additions & 1 deletion examples/sdk_examples/records/list_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,4 +452,3 @@ def main() -> None:

if __name__ == "__main__":
main()

Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ def __init__(self):
parser = argparse.ArgumentParser(prog='biometric update-name', description='Update friendly name of a biometric passkey')
super().__init__(parser)

# def get_parser(self):
# return self.parser

def execute(self, context: KeeperParams, **kwargs):
"""Execute biometric update-name command"""
def _update_name():
Expand Down Expand Up @@ -147,4 +144,4 @@ def _report_update_results(self, result, credential, new_name):
print(f"Old Name: {credential['name']}")
print(f"New Name: {new_name}")
print(f"Message: {result['message']}")
print("=" * 30)
print("=" * 30)
29 changes: 14 additions & 15 deletions keepercli-package/src/keepercli/commands/enterprise_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,22 +1054,21 @@ def _get_ecc_data_keys(self, context: KeeperParams, user_ids: Set[int]) -> Dict[
data_key_rq.enterpriseUserId.extend(user_ids)
data_key_rs = context.auth.execute_auth_rest(
GET_ENTERPRISE_USER_DATA_KEY_ENDPOINT, data_key_rq,
response_type=enterprise_pb2.EnterpriseUserDataKeys)
response_type=APIRequest_pb2.EnterpriseUserIdDataKeyPair)

for key in data_key_rs.keys:
enc_data_key = key.userEncryptedDataKey
if enc_data_key:
try:
ephemeral_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
curve, enc_data_key[:ECC_PUBLIC_KEY_LENGTH])
shared_key = ecc_private_key.exchange(ec.ECDH(), ephemeral_public_key)
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(shared_key)
enc_key = digest.finalize()
data_key = utils.crypto.decrypt_aes_v2(enc_data_key[ECC_PUBLIC_KEY_LENGTH:], enc_key)
data_keys[key.enterpriseUserId] = data_key
except Exception as e:
logger.debug(e)
enc_data_key = data_key_rs.encryptedDataKey
if enc_data_key:
try:
ephemeral_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
curve, enc_data_key[:ECC_PUBLIC_KEY_LENGTH])
shared_key = ecc_private_key.exchange(ec.ECDH(), ephemeral_public_key)
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(shared_key)
enc_key = digest.finalize()
data_key = utils.crypto.decrypt_aes_v2(enc_data_key[ECC_PUBLIC_KEY_LENGTH:], enc_key)
data_keys[data_key_rs.enterpriseUserId] = data_key
except Exception as e:
logger.debug(e)

return data_keys

Expand Down
17 changes: 17 additions & 0 deletions keepercli-package/src/keepercli/commands/pam/debug/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import annotations
from keepersdk.helpers.keeper_dag.dag_utils import value_to_boolean
import os
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from ....params import KeeperParams
from keepersdk.helpers.keeper_dag.connection import ConnectionBase


def get_connection(context: KeeperParams) -> ConnectionBase:
if value_to_boolean(os.environ.get("USE_LOCAL_DAG", False)) is False:
from keepersdk.helpers.keeper_dag.connection.commander import Connection as CommanderConnection
return CommanderConnection(context=context)
else:
from keepersdk.helpers.keeper_dag.connection.local import Connection as LocalConnection
return LocalConnection()
156 changes: 156 additions & 0 deletions keepercli-package/src/keepercli/commands/pam/debug/debug_acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

import logging
import re
import argparse
import time

from ....params import KeeperParams
from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY
from keepersdk.helpers.keeper_dag.record_link import RecordLink
from keepersdk.helpers.keeper_dag.dag_types import UserAcl
from keepersdk.helpers.keeper_dag.dag import EdgeType
from keepersdk.helpers.keeper_dag.dag_types import DiscoveryObject
from keepersdk.helpers.keeper_dag.infrastructure import Infrastructure
from keepersdk.helpers.keeper_dag.user_service import UserService
from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase
from .... import api

logger = api.get_logger()

class PAMDebugACLCommand(PAMGatewayActionDiscoverCommandBase):
def __init__(self):
parser = argparse.ArgumentParser(prog='pam action debug acl')
PAMDebugACLCommand.add_arguments_to_parser(parser)
super().__init__(parser)

@staticmethod
def add_arguments_to_parser(parser: argparse.ArgumentParser):
parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store',
help='Gateway name or UID.')
parser.add_argument('--configuration-uid', "-c", required=False, dest='configuration_uid',
action='store', help='PAM configuration UID, if gateway has multiple.')
parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store',
help='User UID.')
parser.add_argument('--parent-uid', '-r', required=True, dest='parent_uid', action='store',
help='Resource or Configuration UID.')
parser.add_argument('--debug-gs-level', required=False, dest='debug_level', action='store',
help='GraphSync debug level. Default is 0', type=int, default=0)

def execute(self, context: KeeperParams, **kwargs):

gateway = kwargs.get("gateway")
user_uid = kwargs.get("user_uid")
parent_uid = kwargs.get("parent_uid")
debug_level = int(kwargs.get("debug_level", 0))

logger.info("")

configuration_uid = kwargs.get('configuration_uid')

gateway_context = GatewayContext.from_gateway(context=context,
gateway=gateway,
configuration_uid=configuration_uid)
if gateway_context is None:
logger.error(f"Could not find the gateway configuration for {gateway}.")
return

record_link = RecordLink(record=gateway_context.configuration,
context=context,
logger=logger,
debug_level=debug_level)

user_record = context.vault.vault_data.load_record(user_uid)
if user_record is None:
logger.error(f"The user record does not exists.")
return

logger.info(f"The user record is {user_record.title}")

if user_record.record_type != PAM_USER:
logger.error(f"The user record is not a PAM User record.")
return

parent_record = context.vault.vault_data.load_record(parent_uid)
if parent_record is None:
logger.error(f"The parent record does not exists.")
return

logger.info(f"The parent record is {parent_record.title}")

if parent_record.record_type.startswith("pam") is False:
logger.error(f"The parent record is not a PAM record.")
return

if parent_record.record_type == PAM_USER:
logger.error(f"The parent record cannot be a PAM User record.")
return

parent_is_config = parent_record.record_type.endswith("Configuration")

# Get the ACL between the user and the parent.
# It might not exist.
acl_exists = True
acl = record_link.get_acl(user_uid, parent_uid)
if acl is None:
logger.info("No existing ACL, creating an ACL.")
acl = UserAcl()
acl_exists = False

# Make sure the ACL for cloud user is set.
if parent_is_config is True:
logger.info("Is an IAM user.")
acl.is_iam_user = True

rl_parent_vertex = record_link.dag.get_vertex(parent_uid)
if rl_parent_vertex is None:
logger.info("Parent record linking vertex did not exists, creating one.")
rl_parent_vertex = record_link.dag.add_vertex(parent_uid)

rl_user_vertex = record_link.dag.get_vertex(user_uid)
if rl_user_vertex is None:
logger.info("User record linking vertex did not exists, creating one.")
rl_user_vertex = record_link.dag.add_vertex(user_uid)

has_admin_uid = record_link.get_admin_record_uid(parent_uid)
if has_admin_uid is not None:
logger.info("Parent record already has an admin.")
else:
logger.info("Parent record does not have an admin.")

belongs_to_vertex = record_link.acl_has_belong_to_record_uid(user_uid)
if belongs_to_vertex is None:
logger.info("User record does not belong to any resource, or provider.")
else:
if not belongs_to_vertex.active:
logger.info("User record belongs to an inactive parent.")
else:
logger.info("User record belongs to another record.")

logger.info("")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty iinfo?


while True:
res = input(f"Does this user belong to {parent_record.title} Y/N >").lower()
Copy link
Copy Markdown

@sali-ks sali-ks Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create helper method yes/no checks Also support both 'y' and 'yes' as an input

if res == "y":
acl.belongs_to = True
break
elif res == "n":
acl.belongs_to = False
break

if has_admin_uid is None:
while True:
res = input(f"Is this user the admin of {parent_record.title} Y/N >").lower()
if res == "y":
acl.is_admin = True
break
elif res == "n":
acl.is_admin = False
break

try:
record_link.belongs_to(user_uid, parent_uid, acl=acl)
record_link.save()
logger.info(f"Updated/added ACL between {user_record.title} and "
f"{parent_record.title}")
except Exception as err:
logger.error(f"Could not update ACL: {err}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import argparse

from ....params import KeeperParams
from keepersdk.helpers.keeper_dag.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY
from keepersdk.helpers.keeper_dag.record_link import RecordLink
from keepersdk.helpers.keeper_dag.infrastructure import Infrastructure
from keepersdk.helpers.keeper_dag.user_service import UserService
from ..discovery.__init__ import GatewayContext, PAMGatewayActionDiscoverCommandBase
from .... import api
from .debug_graph import PAMDebugGraphCommand

logger = api.get_logger()

class PAMDebugGatewayCommand(PAMGatewayActionDiscoverCommandBase):

type_name_map = {
PAM_USER: "PAM User",
PAM_MACHINE: "PAM Machine",
PAM_DATABASE: "PAM Database",
PAM_DIRECTORY: "PAM Directory",
}

def __init__(self):
parser = argparse.ArgumentParser(prog='pam action debug gateway')
PAMDebugGatewayCommand.add_arguments_to_parser(parser)
super().__init__(parser)

@staticmethod
def add_arguments_to_parser(parser: argparse.ArgumentParser):
parser.add_argument('--gateway', '-g', required=True, dest='gateway', action='store',
help='Gateway name or UID')
parser.add_argument('--configuration-uid', "-c", required=False, dest='configuration_uid',
action='store', help='PAM configuration UID, if gateway has multiple.')

def execute(self, context: KeeperParams, **kwargs):

gateway = kwargs.get("gateway")
debug_level = kwargs.get("debug_level", False)

configuration_uid = kwargs.get('configuration_uid')
vault = context.vault

gateway_context = GatewayContext.from_gateway(context=context,
gateway=gateway,
configuration_uid=configuration_uid)
if gateway_context is None:
logger.error(f"Could not find the gateway configuration for {gateway}.")
return

infra = Infrastructure(record=gateway_context.configuration, vault=vault, fail_on_corrupt=False)
infra.load()

record_link = RecordLink(record=gateway_context.configuration, vault=vault, fail_on_corrupt=False)
user_service = UserService(record=gateway_context.configuration, vault=vault, fail_on_corrupt=False)

if gateway_context is None:
logger.error(f"Cannot get gateway information. Gateway may not be up.")
return

logger.info("")
logger.info(f"Gateway Information")
logger.info(f" Gateway UID: {gateway_context.gateway_uid}")
logger.info(f" Gateway Name: {gateway_context.gateway_name}")
if gateway_context.configuration is not None:
logger.info(f" Configuration UID: {gateway_context.configuration_uid}")
logger.info(f" Configuration Title: {gateway_context.configuration.title}")
logger.info(f" Configuration Key Bytes Hex: {gateway_context.configuration.record_key.hex()}")
else:
logger.error(f"The gateway appears to not have a configuration.")
logger.info("")

graph = PAMDebugGraphCommand()

if infra.dag.has_graph is True:
logger.info(f"Infrastructure Graph")
graph.do_list(context=context, gateway_context=gateway_context, graph_type="infra", debug_level=debug_level,
indent=1)
else:
logger.error(f"The gateway configuration does not have a infrastructure graph.")

logger.info("")

if record_link.dag.has_graph is True:
logger.info(f"Record Linking Graph")
graph.do_list(context=context, gateway_context=gateway_context, graph_type="rl", debug_level=debug_level,
indent=1)
else:
logger.error(f"The gateway configuration does not have a record linking graph.")

logger.info("")

if user_service.dag.has_graph is True:
logger.info(f"User to Service/Task Graph")
graph.do_list(context=context, gateway_context=gateway_context, graph_type="service", debug_level=debug_level,
indent=1)
else:
logger.error(f"The gateway configuration does not have a user to service/task graph.")

logger.info("")
Loading