Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Makefile
dr-logs
/.venv*
*.swp
CLAUDE.md
#CLAUDE.md
AGENTS.md
keeper_db.sqlite
__pycache__/
Expand Down
97 changes: 97 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## ⚠️ Do not edit vendored directories

**Never edit files under `keepercommander/discovery_common/` or `keepercommander/keeper_dag/`.** These are copied in from external "golden" repositories (`keeper-dag`, and the shared Gateway/KDNRM `discovery-common` code). Any edits made here will be **overwritten** the next time the directories are synced from upstream. If a change is needed in this code, make it in the upstream repo — not here. The same applies to generated protobuf files in `keepercommander/proto/` (`*_pb2.py` / `*.pyi`): regenerate from the `.proto` source, never hand-edit.

## What this is

Keeper Commander is a command-line and terminal-UI client for Keeper Password Manager and KeeperPAM. It is a Python package (`keepercommander`) that ships as the `keeper` console script. Beyond vault access it does enterprise administration, PAM (privileged access: rotation, tunnels, discovery), data import/export from other password managers, and can run as a REST service.

## Setup & running

```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pip install -e .
pip install -e '.[email]' # optional email-sending extras
```

Run the CLI:
- `keeper help` — list all commands
- `keeper shell` — interactive command shell
- `keeper supershell` — full terminal vault UI (Textual)
- `keeper <command> [args]` — run a single command and exit
- `python keeper.py ...` — equivalent entry point (calls `keepercommander.__main__:main`)

Config defaults to `config.json` (cwd or platform data dir); override with `KEEPER_CONFIG_FILE`. Set `KEEPER_COMMANDER_DEBUG` for debug logging.

## Testing

CI runs only `unit-tests/` on Python 3.8 and 3.14 (`.github/workflows/test-with-pytest.yml`). Supported range is 3.8–3.14.

```bash
pip install '.[test]'
pytest unit-tests/ # what CI runs
pytest unit-tests/test_sync_down.py # single file
pytest unit-tests/test_sync_down.py::TestClass::test_name # single test
pytest -m keeper_imports # smoke test: every module imports cleanly
```

Test marker semantics (see `pytest.ini`):
- `unit` — mocked, no live server (credential-provision)
- `e2e` — end-to-end, run manually (`pytest -m e2e`)
- `integration` — hits internal `dev.keepersecurity.com` accounts via a `config.json` (`tests/data_config.py`); not for general use
- `cross_enterprise` — excluded by default in `addopts`

Note: `pytest.ini` excludes `venv/`, ignores `unit-tests/test_command_utils.py` (circular import) and `unit-tests/test_login.py` (connection errors), and disables warnings. The `tests/` directory holds heavier integration/e2e suites that are *not* run by CI.

Lint config is `pylintrc`; `desired-pylint-warnings` documents which warning categories the team cares about.

## Architecture

**Entry & dispatch.** `__main__.py` loads config into a `KeeperParams`, then `cli.py` drives the REPL/one-shot execution. `cli.do_command()` is the central dispatcher: it parses a command line, resolves aliases, picks the right registry (`commands`, `enterprise_commands`, or `msp_commands`), and calls the command. Control characters in command input are rejected at this boundary.

**Commands.** Every command subclasses `Command` (or `ArgparseCommand`) in `keepercommander/commands/base.py`. The contract:
- `get_parser()` returns an `argparse.ArgumentParser`; `execute_args()` parses the raw string and dispatches to `execute(params, **kwargs)`.
- `is_authorised()` gates whether login is required.
- `GroupCommand` / `GroupCommandNew` compose sub-verbs (e.g. `pam <verb>`); they own their own sub-registries and aliases.
- Mixins `RecordMixin` / `FolderMixin` provide shared record/folder resolution helpers.

Commands are registered through `register_commands(commands, aliases, command_info)` in `commands/base.py`, which imports each command module and calls its `register_commands` / `register_command_info`. To add a command, create the module, implement the `Command`, and wire it into the appropriate `register_commands` function. The `commands/` subdirectories group large feature areas (`pam`, `pam_cloud`, `pam_import`, `discover`, `enterprise*`, `domain_management`, `remote_management`, `keeper_drive`, `pedm`, `scim`).

**Session & state.** `KeeperParams` (`params.py`) is the single mutable object threaded through everything: session tokens, the in-memory vault cache (records, folders, shared folders, teams), enterprise data, and `RestApiContext` (server, transmission/encryption keys, QRC/EC key negotiation). Commands read and mutate this object rather than passing data around.

**Network & data sync.** `api.py` is the transport layer: `login()`, `communicate()` / `communicate_rest()` (protobuf request/response with throttle retry), and `query_enterprise()`. `rest_api.py` / `loginv3.py` handle the low-level REST and login-v3 flows; `auth/` holds login-step and console-UI logic. `sync_down.py` pulls and decrypts the vault into `params`, then `prepare_folder_tree()` builds the folder hierarchy. Wire formats live in `proto/` (generated `*_pb2.py` — do not hand-edit). Crypto primitives are in `crypto.py`.

**Vault data model.** `vault.py` defines the record types: `KeeperRecord` (abstract) with `PasswordRecord` (v2), `TypedRecord` (v3, field-based with `TypedField`), `FileRecord`, `ApplicationRecord`. `record_facades.py` / `vault_extensions.py` provide typed views; `subfolder.py` models the folder tree.

**Local storage.** `storage/` (SQLite + in-memory DAOs) and `config_storage/` persist cache and config; secure config storage can be encrypted (`loader.SecureStorageException` path in `__main__.py`).

**PAM / discovery / graph.** `keeper_dag/` and `discovery_common/` implement the directed-acyclic graph (DAG) backing PAM discovery, record-linking, and rotation. `commands/pam/`, `commands/pam_cloud/`, and `commands/discover/` build on top of them. These two directories are **vendored copies** of external golden repos — see the warning at the top of this file before touching them.

**Importers.** `importer/` has per-product subpackages (1password, bitwarden, lastpass, keepass, dashlane, proton, thycotic, cyberark, etc.) plus generic csv/json. `imp_exp.py` orchestrates import/export.

**Service mode.** `service/` is a Flask-based REST API server exposing Commander commands over HTTP with API-key auth, rate limiting, and optional response encryption. See `keepercommander/service/README.md`. Managed via `service-create` / `service-start` / etc. commands.

**Plugins.** `plugins/` are rotation plugins loaded dynamically and registered like other commands.

## Style

Follow [PEP 8](https://peps.python.org/pep-0008/), with the project-specific settings enforced by `pylintrc`:
- **Line length: 100** (not PEP 8's default 79).
- `snake_case` for functions, methods, arguments, variables, and attributes.
- `PascalCase` for classes.
- `UPPER_CASE` for module-level and class constants.
- 4-space indentation, no tabs.

Run `pylint keepercommander/<module>.py` to check; `desired-pylint-warnings` documents which warning categories the team treats as meaningful.

## Conventions

- Match the surrounding file's style; most modules carry the Keeper ASCII-art header.
- Never edit `keeper_dag/`, `discovery_common/`, or generated `proto/` files (see the warning at the top of this file).
- Version lives in `keepercommander/__init__.py` (`__version__`); `setup.cfg` reads it via `attr:`.
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.6'
__version__ = '18.0.7'
40 changes: 40 additions & 0 deletions keepercommander/auth/console_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,31 @@ def _stderr(msg=''):
print(msg, file=sys.stderr)


_HEADLESS_AUTH_MSG_SHOWN = False


def _is_interactive():
try:
return bool(sys.stdin) and sys.stdin.isatty()
except Exception:
return False


def _fail_headless_auth(step):
"""In headless/service mode, persistent login often needs a follow-up prompt
(password, SSO, 2FA, device approval) that cannot be answered. Log once and
cancel so the caller exits cleanly instead of looping or spamming getpass."""
global _HEADLESS_AUTH_MSG_SHOWN
if not _HEADLESS_AUTH_MSG_SHOWN:
_HEADLESS_AUTH_MSG_SHOWN = True
logging.error(
'Persistent login is not working in this non-interactive environment '
'(possibly due to an IP/location change). '
'Re-run Commander/Docker setup from this network, then restart the service.'
)
step.cancel()


class ConsoleLoginUi(login_steps.LoginUi):
def __init__(self):
self._show_device_approval_help = True
Expand All @@ -28,6 +53,9 @@ def __init__(self):
self._failed_password_attempt = 0

def on_device_approval(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
if self._show_device_approval_help:
_stderr(f"\n{Fore.YELLOW}Device Approval Required{Fore.RESET}\n")
_stderr(f"{Fore.CYAN}Select an approval method:{Fore.RESET}")
Expand Down Expand Up @@ -123,6 +151,9 @@ def two_factor_channel_to_desc(channel): # type: (login_steps.TwoFactorChannel
return 'Backup Codes'

def on_two_factor(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
channels = step.get_channels()

if self._show_two_factor_help:
Expand Down Expand Up @@ -273,6 +304,9 @@ def on_two_factor(self, step):
logging.warning(f'{Fore.YELLOW}Invalid 2FA code. Please try again.{Fore.RESET}')

def on_password(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
if self._show_password_help:
_stderr(f'{Fore.CYAN}Enter master password for {Fore.WHITE}{step.username}{Fore.RESET}')

Expand All @@ -293,6 +327,9 @@ def on_password(self, step):
step.cancel()

def on_sso_redirect(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
try:
wb = webbrowser.get()
wrappers = set('xdg-open|gvfs-open|gnome-open|x-www-browser|www-browser'.split('|'))
Expand Down Expand Up @@ -360,6 +397,9 @@ def on_sso_redirect(self, step):
break

def on_sso_data_key(self, step):
if not _is_interactive():
_fail_headless_auth(step)
return
if self._show_sso_data_key_help:
_stderr(f'\n{Fore.YELLOW}Device Approval Required for SSO{Fore.RESET}\n')
_stderr(f'{Fore.CYAN}Select an approval method:{Fore.RESET}')
Expand Down
7 changes: 7 additions & 0 deletions keepercommander/commands/discoveryrotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import re
import time
from datetime import datetime
from typing import Dict, Optional, Any, Set, List
from urllib.parse import urlparse, urlunparse

import requests
Expand Down Expand Up @@ -89,10 +90,12 @@
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
)
from .pam.cnapp_commands import PAMCnappCommand

# These characters are based on the Vault
PAM_DEFAULT_SPECIAL_CHAR = '''!@#$%^?();',.=+[]<>{}-_/\\*&:"`~|'''
Expand Down Expand Up @@ -200,8 +203,12 @@ 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 Cloud-Native Application Protection Platform integration', 'cn')


class PAMGatewayCommand(GroupCommand):
Expand Down
17 changes: 17 additions & 0 deletions keepercommander/commands/helpers/record.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import re
from typing import Set, Optional

from ... import api
from ...error import CommandError
from ...params import KeeperParams
from ...subfolder import try_resolve_path

# Block shell chaining markers in `get` lookup tokens.
_GET_LOOKUP_CONTROL_CHARS_RE = re.compile(r'[\r\n\x00]')
_GET_LOOKUP_SHELL_METACHAR_RE = re.compile(r'[;|]')
_GET_LOOKUP_CHAIN_RE = re.compile(r'&&')


def raise_if_unsafe_get_lookup_token(token, command='get'):
# type: (str, str) -> None
if not token:
raise CommandError(command, 'Invalid record identifier: forbidden characters')
if (_GET_LOOKUP_CONTROL_CHARS_RE.search(token)
or _GET_LOOKUP_SHELL_METACHAR_RE.search(token)
or _GET_LOOKUP_CHAIN_RE.search(token)):
raise CommandError(command, 'Invalid record identifier: forbidden characters')


# Get record UID(s) given one of its identifiers: name (if current folder contains the record), path, or UID
def get_record_uids(params, name): # type: (KeeperParams, str) -> Set[Optional[str]]
Expand Down
Loading
Loading