Skip to content
Merged

Release #2187

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4fdf9e2
Migrate from the user service graph to the PAM graph.
jwalstra-keeper Mar 5, 2026
14999e2
fix: Lowercase the OS type when checking if "windows"
jwalstra-keeper Jun 24, 2026
0c51824
Fix: Restrict non-owner from attaching pam scripts (#2175)
amangalampalli-ks Jun 29, 2026
88e284b
feat: Add command `pam action service map` to map users to discovered…
jwalstra-keeper Jun 30, 2026
9d5ed3b
KC-1335, KC-1336: Fix nsf-mkdir parent UID path resolution and get to…
sshrushanth-ks Jul 1, 2026
e3b5ed2
Fix KEEPER_SSL_CERT_FILE ignored for HTTP SSL verification (#2173)
amangalampalli-ks Jul 2, 2026
cd71f31
mc_transfer_perform to should respect 'treeKeyTypeId' property. KC-1341
sk-keeper Jul 2, 2026
ade2f8a
Release 18.0.10
sk-keeper Jul 2, 2026
3d31a73
Add domain user match to PAM domain configuration
idimov-keeper Jun 30, 2026
93301c6
Support clearing domain config fields on pam config edit
idimov-keeper Jun 30, 2026
0291cfb
KC-1330 Launch credential not attached on first pam connection edit; …
idimov-keeper Jun 30, 2026
ee5bd5e
Fix pam rotation edit --schedule-config and scripts_only setup (KC-1331)
idimov-keeper Jul 1, 2026
59864d5
Add use_default_rotation_schedule to pam rotation info JSON output
idimov-keeper Jul 1, 2026
b8622d7
Fix iam_user rotation schedule on initial create (KC-1337)
idimov-keeper Jul 1, 2026
70f69cc
feat: Add RDP/Kubernetes security options to pam connection edit
idimov-keeper Jul 2, 2026
1b4d5b9
fix: Align pam launch protocol extractors with record JSON keys
idimov-keeper Jul 2, 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
2 changes: 1 addition & 1 deletion keepercommander/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
# Contact: commander@keepersecurity.com
#

__version__ = '18.0.9'
__version__ = '18.0.10'
2 changes: 1 addition & 1 deletion keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ def execute_router_rest(params: KeeperParams, endpoint: str, payload: Optional[b
if payload is not None:
payload = crypto.encrypt_aes_v2(payload, transmission_key)
rs = requests.post(url, data=payload, headers=headers, proxies=params.rest_context.proxies,
verify=params.rest_context.certificate_check)
verify=params.ssl_verify)
if rs.status_code == 200:
rs_body = rs.content
if rs_body:
Expand Down
12 changes: 9 additions & 3 deletions keepercommander/attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def __init__(self):
self.name = ''
self.title = ''
self.thumbnail = None # type: Optional[bytes]
self.is_script = False

def prepare(self):
pass
Expand All @@ -150,10 +151,11 @@ def open(self):


class FileUploadTask(UploadTask):
def __init__(self, file_path):
def __init__(self, file_path, is_script=False):
super().__init__()
self.file_path = file_path
self.name = os.path.basename(self.file_path)
self.is_script = is_script

def prepare(self):
self.file_path = os.path.expanduser(self.file_path)
Expand Down Expand Up @@ -247,6 +249,7 @@ def upload_attachments(params, record, attachments):
file.record_uid = file_uid
file.record_key = crypto.encrypt_aes_v2(file_key, params.data_key)
file.fileSize = task.size + 100
file.is_script = task.is_script
file_data = {
'title': task.title or task.name,
'name': task.name or '',
Expand All @@ -267,6 +270,9 @@ def upload_attachments(params, record, attachments):
file_uid = uo.record_uid
task = file_tasks[file_uid]
if uo.status != record_pb2.FA_SUCCESS:
if task.is_script:
raise Exception(
f'Uploading rotation script {task.name}: only the record owner can attach post-rotation scripts.')
raise Exception(f'Uploading file {task.name}: Get upload URL error.')

file_key = file_keys[file_uid]
Expand All @@ -278,7 +284,7 @@ def upload_attachments(params, record, attachments):
}
response = requests.post(uo.url, files=files, data=json.loads(uo.parameters),
proxies=params.rest_context.proxies,
verify=params.rest_context.certificate_check)
verify=params.ssl_verify)
if response.status_code == uo.success_status_code:
facade.file_ref.append(file_ref)
if record.linked_keys is None:
Expand All @@ -295,7 +301,7 @@ def upload_attachments(params, record, attachments):
}
requests.post(uo.url, files=files, data=json.loads(uo.thumbnail_parameters),
proxies=params.rest_context.proxies,
verify=params.rest_context.certificate_check)
verify=params.ssl_verify)
except Exception as e:
logging.warning('Error uploading thumbnail: %s', e)
else:
Expand Down
66 changes: 65 additions & 1 deletion keepercommander/commands/discover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ..pam.config_facades import PamConfigurationRecordFacade
from ..pam.router_helper import get_response_payload
from ..pam.gateway_helper import get_all_gateways
from ..pam.router_helper import router_send_action_to_gateway
from ..ksm import KSMCommand
from ... import utils, vault_extensions
from ... import vault
Expand All @@ -14,16 +15,18 @@
from ...display import bcolors
from ...discovery_common.constants import PAM_USER, PAM_MACHINE, PAM_DATABASE, PAM_DIRECTORY
from ...utils import value_to_boolean
from ...proto import pam_pb2
import json
import base64
import re
from packaging import version as packaging_version

from typing import List, Optional, Union, Callable, Tuple, Any, Dict, TYPE_CHECKING

if TYPE_CHECKING:
from ...params import KeeperParams
from ...vault import KeeperRecord, ApplicationRecord
from ...proto import pam_pb2



class MultiConfigurationException(Exception):
Expand Down Expand Up @@ -63,6 +66,7 @@ def __init__(self, configuration: KeeperRecord, facade: PamConfigurationRecordFa
self.gateway = gateway
self.application = application
self._shared_folders = None
self._info: Optional[Dict] = None

@staticmethod
def all_gateways(params: KeeperParams):
Expand Down Expand Up @@ -294,6 +298,66 @@ def get_shared_folders(self, params: KeeperParams) -> List[dict]:
})
return self._shared_folders

def info(self, params: KeeperParams) -> Optional[Dict]:
if self._info is None:
from ..pam.pam_dto import GatewayActionGatewayInfo

if self.gateway_uid is None:
raise Exception("Gateway UID is not set for getting Gateway info.")

router_response = router_send_action_to_gateway(
params=params,
gateway_action=GatewayActionGatewayInfo(is_scheduled=False),
message_type=pam_pb2.CMT_GENERAL,
is_streaming=False,
destination_gateway_uid_str=self.gateway_uid
)

if router_response is None:
print(f"{bcolors.FAIL}Did not get router response.{bcolors.ENDC}")
return

response = router_response.get("response")
logging.debug(f"Router Response: {response}")
payload = get_response_payload(router_response)
data = payload.get("data")
if data is None:
print(f"{bcolors.FAIL}The router returned a failure.{bcolors.ENDC}")
return
elif data.get("success") is False:
error = data.get("error")
logging.debug(f"gateway returned: {error}")
print(f"{bcolors.FAIL}Could not map users to Windows services.{bcolors.ENDC}")

self._info = data

return self._info

def _gateway_version(self, params: KeeperParams) -> Optional[packaging_version.Version]:

try:
info = self.info(params)
gateway_version_str = info["gateway-config"]['version']["current"]
logging.debug(f"gateway version is {gateway_version_str}")
return packaging_version.parse(gateway_version_str)
except Exception as err:
logging.debug(f"got getting trying to get gateway version: {err}")
return None

def gateway_version_gte(self, params: KeeperParams, require_version_str: str) -> bool:
gateway_version = self._gateway_version(params)
if gateway_version is None:
return False
require_version = packaging_version.parse(require_version_str)
return gateway_version >= require_version

def gateway_version_lte(self, params: KeeperParams, require_version_str: str) -> bool:
gateway_version = self._gateway_version(params)
if gateway_version is None:
return False
require_version = packaging_version.parse(require_version_str)
return gateway_version <= require_version

def decrypt(self, cipher_base64: bytes) -> dict:
ciphertext = base64.b64decode(cipher_base64.decode())
return json.loads(decrypt_aes_v2(ciphertext, self.configuration.record_key))
Expand Down
68 changes: 60 additions & 8 deletions keepercommander/commands/discover/result_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from ...discovery_common.dag_sort import sort_infra_vertices
from ...discovery_common.infrastructure import Infrastructure
from ...discovery_common.process import Process, QuitException, NoDiscoveryDataException
from ...discovery_common.record_link import RecordLink
from ...discovery_common.user_service import UserService
from ...discovery_common.types import (
DiscoveryObject, UserAcl, PromptActionEnum, PromptResult, BulkRecordAdd, BulkRecordConvert, BulkProcessResults,
BulkRecordSuccess, BulkRecordFail, DirectoryInfo, NormalizedRecord, RecordField)
Expand Down Expand Up @@ -99,7 +101,7 @@ def _is_directory_user(record_type: str) -> bool:
record_type == "pamAzureConfiguration")

@staticmethod
def _get_shared_folder(params: KeeperParams, pad: str, gateway_context: GatewayContext) -> str:
def _get_shared_folder(params: KeeperParams, pad: str, gateway_context: GatewayContext) -> Optional[str]:
while True:
shared_folders = gateway_context.get_shared_folders(params)
index = 0
Expand All @@ -113,7 +115,7 @@ def _get_shared_folder(params: KeeperParams, pad: str, gateway_context: GatewayC
print(f"{pad}{_f('Input was not a number.')}")

@staticmethod
def get_field_values(record: TypedRecord, field_type: str) -> List[Any]:
def get_field_values(record: TypedRecord, field_type: str) -> Optional[List[Any]]:
return next(
(f.value
for f in record.fields
Expand Down Expand Up @@ -147,7 +149,7 @@ def get_keys_by_record(self, params: KeeperParams, gateway_context: GatewayConte

elif key_field == "host":
values = self.get_field_values(record, "pamHostname")
if len(values) == 0:
if values is None or len(values) == 0:
return []

host = values[0].get("hostName")
Expand All @@ -171,15 +173,17 @@ def get_keys_by_record(self, params: KeeperParams, gateway_context: GatewayConte
return []

values = self.get_field_values(record, "login")
if len(values) == 0:
if values is None or len(values) == 0:
return []

keys.append(f"{resource_uid}:{values[0]}".lower())

return keys

@staticmethod
def _record_lookup(record_uid: str, context: Optional[Any] = None) -> Optional[NormalizedRecord]:
def _record_lookup(record_uid: str,
context: Optional[Any] = None,
allow_sm: bool = False) -> Optional[NormalizedRecord]:

"""
Get the record from the Vault, normalize it, and return it.
Expand Down Expand Up @@ -286,7 +290,7 @@ def _edit_record(self, content: DiscoveryObject, pad: str, editable: List[str])
new_values = map(str.strip, new_value.split(','))
new_value = "\n".join(new_values)
elif type_hint == "multiline":
print(_b(f"{pad}Enter multiline of text or a path, on the first line, "
print(_b(f"{pad}Enter multiple of text or a path, on the first line, "
"to a file that contains the value."))
print(_b(f"{pad}To end, type 'END' at the start of a new line. You can paste text."))
new_value = ""
Expand Down Expand Up @@ -449,8 +453,8 @@ def _prompt_display_relationships(vertex: DAGVertex, content: DiscoveryObject, p
def _prompt(self,
content: DiscoveryObject,
acl: UserAcl,
parent_vertex: DAGVertex,
vertex: Optional[DAGVertex] = None,
parent_vertex: Optional[DAGVertex] = None,
resource_has_admin: bool = True,
item_count: int = 0,
items_left: int = 0,
Expand Down Expand Up @@ -821,6 +825,10 @@ def _handle_admin_record_from_record(record: TypedRecord,
password_field = next((x for x in record.fields if x.type == "password"), None)
private_key_field = next((x for x in record.fields if x.type == "keyPair"), None)

if login_field is None:
logging.error("Record is missing the login field.")
return None

content.set_field_value("login", login_field.value)
if password_field is not None:
content.set_field_value("password", password_field.value)
Expand Down Expand Up @@ -1251,6 +1259,8 @@ def _convert_records(cls, bulk_convert_records: List[BulkRecordConvert], context
for bulk_convert_record in bulk_convert_records:

record = vault.KeeperRecord.load(params, bulk_convert_record.record_uid)
if record is None:
continue

rotation_disabled = False

Expand Down Expand Up @@ -1349,18 +1359,25 @@ def preview(self, job_item: JobItem, params: KeeperParams, gateway_context: Gate
}

sync_point = job_item.sync_point
if sync_point is None:
sync_point = 0
infra = Infrastructure(record=gateway_context.configuration,
params=params,
logger=logging,
debug_level=debug_level)
infra.load(sync_point)

configuration = None
try:
configuration = infra.get_root.has_vertices()[0]
except (Exception,):
print(f"{bcolors.FAIL}Could not find the configuration in the infrastructure graph. "
f"Has discovery been run for this gateway?{bcolors.ENDC}")
return

if configuration is None:
print(f"{bcolors.FAIL}Could not find the configuration in the infrastructure graph. "
f"Has discovery been run for this gateway?{bcolors.ENDC}")
return

record_type_to_vertices_map = sort_infra_vertices(configuration)

Expand Down Expand Up @@ -1482,6 +1499,31 @@ def _print_cloud_user(rt: str, rule_result: str):

print("")

# TODO - `context` is not being passed; run_full might need to change to work with Commander.
def map_users_to_services(self,
params: KeeperParams,
configuration_record: TypedRecord,
debug_level: int = 0):

record_link = RecordLink(record=configuration_record,
params=params,
logger=logging,
debug_level=debug_level)
user_service = UserService(record_linking=record_link,
record=configuration_record,
params=params,
logger=logging,
debug_level=debug_level)

infra = Infrastructure(record=configuration_record,
params=params,
logger=logging,
debug_level=debug_level)
infra.load(0)

user_service.run_full(record_lookup_func=self._record_lookup,
infra=infra)

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

if not hasattr(params, 'pam_controllers'):
Expand Down Expand Up @@ -1609,6 +1651,16 @@ def execute(self, params: KeeperParams, **kwargs):
else:
print(f"{bcolors.FAIL}No records have been added.{bcolors.ENDC}")

# New users may have been added.
# Map the users to Windows services.
# try:
# self.map_users_to_services(params=params,
# configuration_record=configuration_record,
# debug_level=debug_level)
# except Exception as err:
# logging.error(err)
# print(f"{bcolors.FAIL}Could not map users to services.{bcolors.ENDC}")

except NoDiscoveryDataException:
print(f"{bcolors.OKGREEN}All items have been added for this discovery job.{bcolors.ENDC}")
self.remove_job(params=params, configuration_record=configuration_record, job_id=job_id)
Expand Down
Loading
Loading