Skip to content
Merged

Release #1803

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
6 changes: 3 additions & 3 deletions keepercommander/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
# |_|
#
# Keeper Commander
# Copyright 2023 Keeper Security Inc.
# Contact: ops@keepersecurity.com
# Copyright 2009-2026 Keeper Security Inc.
# Contact: commander@keepersecurity.com
#

__version__ = '17.2.6'
__version__ = '17.2.7'
11 changes: 8 additions & 3 deletions keepercommander/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@ def display_command_help(show_enterprise=False, show_shell=False, show_legacy=Fa
from colorama import Fore, Style
import shutil

alias_lookup = {x[1]: x[0] for x in aliases.items()}
# Build a lookup of command -> list of aliases
alias_lookup = {}
for alias, command in aliases.items():
if command not in alias_lookup:
alias_lookup[command] = []
alias_lookup[command].append(alias)
DIM = Fore.WHITE # Use white for better readability (not too bright, not too dim)

# Get terminal width
Expand Down Expand Up @@ -144,8 +149,8 @@ def clean_description(desc):
else:
commands_in_category = sorted(categorized_commands[category], key=lambda x: x[0])
for cmd, description in commands_in_category:
alias = alias_lookup.get(cmd) or ''
alias_str = f' ({alias})' if alias else ''
aliases_list = alias_lookup.get(cmd) or []
alias_str = f' ({", ".join(sorted(aliases_list))})' if aliases_list else ''
cmd_display = f'{cmd}{alias_str}'
all_cmd_displays.append((category, cmd_display, description))
global_max_width = max(global_max_width, len(cmd_display))
Expand Down
2 changes: 1 addition & 1 deletion keepercommander/commands/_supershell_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4201,7 +4201,7 @@ def _update_shell_header(self):
f"[{t['text_dim']}] (Enter to run | Up/Down for history | Ctrl+D to close)[/{t['text_dim']}]"
)

def check_action(self, action: str, parameters: tuple) -> bool | None:
def check_action(self, action: str, parameters: tuple) -> Optional[bool]:
"""Control whether actions are enabled based on search state"""
# When search input is active, disable all bindings except escape and search
# This allows keys to be captured as text input instead of triggering actions
Expand Down
8 changes: 7 additions & 1 deletion keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,10 @@ class PAMConfigurationNewCommand(Command, PamConfigurationEditMixin):
help='Set recording connections permissions for the resource')
parser.add_argument('--typescript-recording', '-tr', dest='typescriptrecording', choices=choices,
help='Set TypeScript recording permissions for the resource')
parser.add_argument('--ai-threat-detection', dest='ai_threat_detection', choices=choices,
help='Set AI threat detection permissions')
parser.add_argument('--ai-terminate-session-on-detection', dest='ai_terminate_session_on_detection', choices=choices,
help='Set AI session termination on threat detection permissions')

def __init__(self):
super().__init__()
Expand Down Expand Up @@ -2274,7 +2278,9 @@ def execute(self, params, **kwargs):
kwargs.get('rotation'),
kwargs.get('recording'),
kwargs.get('typescriptrecording'),
kwargs.get('remotebrowserisolation')
kwargs.get('remotebrowserisolation'),
kwargs.get('ai_threat_detection'),
kwargs.get('ai_terminate_session_on_detection')
)
if admin_cred_ref:
tmp_dag.link_user_to_config_with_options(admin_cred_ref, is_admin='on')
Expand Down
49 changes: 30 additions & 19 deletions keepercommander/commands/pam_debug/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,31 +505,42 @@ def _print_field(f):
print(f" {self._b('Provider Group')}: {content.item.provider_group}")
print(f" {self._b('Allows Admin')}: {content.item.allows_admin}")
print(f" {self._b('Admin Reason')}: {content.item.admin_reason}")
else:
for k, v in content.item:
print(f" {self._b(k)}: {v}")

print("")
print(self._h("Belongs To Vertices (Parents)"))
vertices = discovery_vertex.belongs_to_vertices()
for vertex in vertices:
content = DiscoveryObject.get_discovery_object(vertex)
print(f" * {content.description} ({vertex.uid})")
for edge_type in [EdgeType.LINK, EdgeType.ACL, EdgeType.KEY, EdgeType.DELETION]:
edge = discovery_vertex.get_edge(vertex, edge_type=edge_type)
if edge is not None:
print(f" . {edge_type}, active: {edge.active}")

if len(vertices) == 0:
print(f"{bcolors.FAIL} Does not belong to anyone{bcolors.ENDC}")
# Configuration records do not belong to other record; don't show.
if record.version != 6:
print("")
print(self._h("Belongs To Vertices (Parents)"))
vertices = discovery_vertex.belongs_to_vertices()
for vertex in vertices:
try:
content = DiscoveryObject.get_discovery_object(vertex)
print(f" * {content.description} ({vertex.uid})")
for edge_type in [EdgeType.LINK, EdgeType.ACL, EdgeType.KEY, EdgeType.DELETION]:
edge = discovery_vertex.get_edge(vertex, edge_type=edge_type)
if edge is not None:
print(f" . {edge_type}, active: {edge.active}")
except Exception as err:
print(f"{bcolors.FAIL}Could not get belongs to information: {err}{bcolors.ENDC}")

if len(vertices) == 0:
print(f"{bcolors.FAIL} Does not belong to anyone{bcolors.ENDC}")

print("")
print(f"{bcolors.HEADER}Vertices Belonging To (Children){bcolors.ENDC}")
vertices = discovery_vertex.has_vertices()
for vertex in vertices:
content = DiscoveryObject.get_discovery_object(vertex)
print(f" * {content.description} ({vertex.uid})")
for edge_type in [EdgeType.LINK, EdgeType.ACL, EdgeType.KEY, EdgeType.DELETION]:
edge = vertex.get_edge(discovery_vertex, edge_type=edge_type)
if edge is not None:
print(f" . {edge_type}, active: {edge.active}")
try:
content = DiscoveryObject.get_discovery_object(vertex)
print(f" * {content.description} ({vertex.uid})")
for edge_type in [EdgeType.LINK, EdgeType.ACL, EdgeType.KEY, EdgeType.DELETION]:
edge = vertex.get_edge(discovery_vertex, edge_type=edge_type)
if edge is not None:
print(f" . {edge_type}, active: {edge.active}")
except Exception as err:
print(f"{bcolors.FAIL}Could not get belonging to information: {err}{bcolors.ENDC}")
if len(vertices) == 0:
print(f" Does not have any children.")

Expand Down
120 changes: 118 additions & 2 deletions keepercommander/commands/pam_import/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ _You can have only one `pam_configuration` section and the only required paramet
"connections": "on",
"rotation": "on",
"tunneling": "on",
"ai_threat_detection": "off",
"ai_terminate_session_on_detection": "off",
"remote_browser_isolation": "on",
"graphical_session_recording": "off",
"text_session_recording": "off",
Expand Down Expand Up @@ -201,6 +203,109 @@ Each Machine (pamMachine, pamDatabase, pamDirectory) can specify admin user whic
> **Note 1:** `pam_settings` _(options, connection)_ are explained only in pamMachine section below (per protocol) but they are present in all machine types.
> **Note 2:** `attachments` and `scripts` examples are in `pam_configuration: local` section.
> **Note 3:** Post rotation scripts (a.k.a. `scripts`) are executed in following order: `pamUser` scripts after any **successful** rotation for that user, `pamMachine` scripts after any **successful** rotation on the machine and `pamConfiguration` scripts after any rotation using that configuration.

JIT and KeeperAI settings below are shared across all resource types (pamMachine, pamDatabase, pamDirectory) except User and RBI (pamRemoteBrowser) records.

<details>
<summary>Just-In-Time Access (JIT)</summary>

[Just-In-Time Access (JIT)](https://docs.keeper.io/en/keeperpam/privileged-access-manager/getting-started/just-in-time-access-jit) - By implementing JIT access controls, organizations can significantly reduce their attack surface by ensuring that privileged access is only granted when needed, for the duration required, and with appropriate approvals.

**How to Configure:** Import JSON follows Keeper Vault web UI (JIT tab on resource records). Configure the elevation settings (Ephemeral account or Group/Role elevation) using `pam_settings.options.jit_settings`. Use `pam_directory_record` to reference a pamDirectory by its `title` from `pam_data.resources[]` (for domain account type):

```json
{
"jit_settings": {
"create_ephemeral": true,
"elevate": true,
"_comment_method": "elevation methods: <group|role>",
"elevation_method": "group",
"elevation_string": "arn:aws:iam::12345:role/Admin",
"base_distinguished_name": "OU=Users,DC=example,DC=net",
"_comment_ephemeral_account_types": "<linux|mac|windows|domain>",
"ephemeral_account_type": "linux",
"_comment_pam_directory_record": "by title, requried if ephemeral_account_type: domain",
"pam_directory_record": "PAM AD1"
}
}
```
</details>
<details>
<summary>KeeperAI</summary>

[KeeperAI](https://docs.keeper.io/en/keeperpam/privileged-access-manager/keeperai) - AI-powered threat detection for KeeperPAM privileged sessions. KeeperAI is an Agentic AI-powered threat detection system that automatically monitors and analyzes KeeperPAM privileged sessions to identify suspicious or malicious behavior.

**PAM Configuration Settings** (in `pam_configuration`):
- `ai_threat_detection`
- `ai_terminate_session_on_detection`

**Activating Threat Detection on a Resource:** Import JSON follows Keeper Vault web UI (AI tab on resource records). Session recordings (graphical and/or text) must be enabled for KeeperAI to work. Edit PAM Settings for your selected resource: enable `ai_threat_detection` and `ai_terminate_session_on_detection` in `pam_settings.options`, then add `pam_settings.options.ai_settings` with your risk-level rules:

```json
{
"pam_settings": {
"options": {
"graphical_session_recording": "on",
"text_session_recording": "on",
"ai_threat_detection": "on",
"ai_terminate_session_on_detection": "on",
"ai_settings": {
"risk_levels": {
"critical": {
"ai_session_terminate": true,
"activities": {
"allow": [
{"tag": "mount"},
{"tag": "umount"}
],
"deny": [
{"tag": "iptables"},
{"tag": "wget | sh"}
]
}
},
"high": {
"ai_session_terminate": true,
"activities": {
"allow": [
{"tag": "\\bmount\\b"},
{"tag": "\\bumount\\b"}
],
"deny": [
{"tag": "kill -9"},
{"tag": "\\bkill\\s+-9\\b.*"}
]
}
},
"medium": {
"ai_session_terminate": true,
"activities": {
"allow": [
{"tag": "chmod"},
{"tag": "chown"}
],
"deny": [
{"tag": "bash"},
{"tag": "dash"}
]
}
},
"low": {
"ai_session_terminate": false,
"activities": {
"allow": [
{"tag": "\\bwget\\b"},
{"tag": "\\bchmod\\b"}
]
}
}
}
}
}
}
}
```
</details>
<details>
<summary>pam_data.resources.pamMachine (RDP)</summary>

Expand All @@ -211,6 +316,7 @@ Each Machine (pamMachine, pamDatabase, pamDirectory) can specify admin user whic
"notes": "RDP Machine1",
"host": "127.0.0.1",
"port": "3389",
"_comment_port": "administrative port",
"ssl_verification" : true,
"operating_system": "Windows",
"instance_name": "InstanceName",
Expand All @@ -227,16 +333,22 @@ Each Machine (pamMachine, pamDatabase, pamDirectory) can specify admin user whic
"tunneling": "on",
"remote_browser_isolation": "on",
"graphical_session_recording": "on",
"ai_threat_detection": "off",
"ai_terminate_session_on_detection": "off",
"jit_settings": {},
"ai_settings": {}
},
"allow_supply_host": false,
"port_forward": {
"_comment": "Tunneling settings",
"_comment_port": "remote tunneling port",
"port": "2222",
"reuse_port": true
},
"connection" : {
"_comment": "Connections settings per protocol - RDP",
"protocol": "rdp",
"_comment_port": "connection port",
"port": "2222",
"allow_supply_user": true,
"administrative_credentials": "admin1",
Expand Down Expand Up @@ -292,7 +404,9 @@ Each Machine (pamMachine, pamDatabase, pamDirectory) can specify admin user whic
"tunneling": "on",
"remote_browser_isolation": "on",
"graphical_session_recording": "on",
"text_session_recording": "on"
"text_session_recording": "on",
"ai_threat_detection": "off",
"ai_terminate_session_on_detection": "off"
},
"allow_supply_host": false,
"port_forward": {
Expand Down Expand Up @@ -517,7 +631,9 @@ Each Machine (pamMachine, pamDatabase, pamDirectory) can specify admin user whic
"tunneling": "on",
"remote_browser_isolation": "on",
"graphical_session_recording": "on",
"text_session_recording": "on"
"text_session_recording": "on",
"ai_threat_detection": "off",
"ai_terminate_session_on_detection": "off"
},
"allow_supply_host": false,
"port_forward": {
Expand Down
Loading
Loading