Skip to content
Merged
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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.DS_Store
*.iml
.cache/
.cursor/
venv/
*.json
*.pyc
Expand All @@ -19,4 +20,9 @@ dr-logs
CLAUDE.md
AGENTS.md
keeper_db.sqlite
.keeper-memory-mcp/
__pycache__/
.keeper-memory-mcp/
.mcp/
tests/*_results/
tests/*.log
tests/compliance_test.env
16 changes: 12 additions & 4 deletions RECORD_ADD_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ record-add --title "Record Title" --record-type "RECORD_TYPE" [OPTIONS] [FIELDS.
**Special Value Syntax:**
- `$JSON:{"key": "value"}` - For complex object fields
- `$GEN` - Generate passwords, TOTP codes, or key pairs
- `$BASE64:<base64_string>` - Decode base64-encoded values for any string field
- `file=@filename` - File attachments

## Record Types
Expand Down Expand Up @@ -511,10 +512,11 @@ echo "Emergency database access: $URL" | secure-send user@company.com
1. **Use single-line commands for copy-paste** to avoid trailing space issues
2. **Quote JSON values** to prevent shell interpretation
3. **Use $GEN for passwords** instead of hardcoding them
4. **Test with simple records first** before creating complex ones
5. **Use custom fields (c.) for non-standard data**
6. **Organize records in folders** using the `--folder` parameter
7. **Add meaningful notes** with `--notes` for context
4. **Use $BASE64: for complex passwords** with special characters to avoid shell escaping issues
5. **Test with simple records first** before creating complex ones
6. **Use custom fields (c.) for non-standard data**
7. **Organize records in folders** using the `--folder` parameter
8. **Add meaningful notes** with `--notes` for context

## Troubleshooting

Expand All @@ -538,6 +540,12 @@ echo "Emergency database access: $URL" | secure-send user@company.com
- Ensure file path is accessible
- Use absolute paths to avoid confusion

**Base64 decoding errors**
- Ensure the base64 string is valid (test with `echo <string> | base64 -d`)
- Use the `$BASE64:` prefix: `password='$BASE64:UEBzc3cwcmQh'`
- Remove any newlines or spaces from the base64 string
- Check that the decoded value is valid UTF-8 text

## Record-Update vs Record-Add

While `record-add` creates new records, `record-update` modifies existing records. Here's how they compare:
Expand Down
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__ = '17.2.7'
__version__ = '17.2.8'
32 changes: 19 additions & 13 deletions keepercommander/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,21 +493,24 @@ def search_shared_folders(params, searchstring, use_regex=False):
params: KeeperParams
searchstring: Search string (tokens or regex depending on use_regex)
use_regex: If True, treat as regex. If False (default), token-based search.
If searchstring is empty, returns all shared folders.
"""
search_results = []

if not searchstring:
return search_results

if use_regex:
# No search string - return all shared folders
match_func = lambda target: True
elif use_regex:
p = re.compile(searchstring.lower())
match_func = lambda target: p.search(target)
else:
# Token-based search: all tokens must match
tokens = [t.lower() for t in searchstring.split() if t.strip()]
if not tokens:
return search_results
match_func = lambda target: all(token in target for token in tokens)
# No valid tokens - return all shared folders
match_func = lambda target: True
else:
match_func = lambda target: all(token in target for token in tokens)

for shared_folder_uid in params.shared_folder_cache:

Expand All @@ -529,21 +532,24 @@ def search_teams(params, searchstring, use_regex=False):
params: KeeperParams
searchstring: Search string (tokens or regex depending on use_regex)
use_regex: If True, treat as regex. If False (default), token-based search.
If searchstring is empty, returns all teams.
"""
search_results = []

if not searchstring:
return search_results

if use_regex:
# No search string - return all teams
match_func = lambda target: True
elif use_regex:
p = re.compile(searchstring.lower())
match_func = lambda target: p.search(target)
else:
# Token-based search: all tokens must match
tokens = [t.lower() for t in searchstring.split() if t.strip()]
if not tokens:
return search_results
match_func = lambda target: all(token in target for token in tokens)
# No valid tokens - return all teams
match_func = lambda target: True
else:
match_func = lambda target: all(token in target for token in tokens)

for team_uid in params.team_cache:
team = get_team(params, team_uid)
Expand Down Expand Up @@ -806,7 +812,7 @@ def execute_router_json(params, endpoint, request):
return None


def communicate_rest(params, request, endpoint, *, rs_type=None, payload_version=None):
def communicate_rest(params, request, endpoint, *, rs_type=None, payload_version=None, timeout=None):
api_request_payload = APIRequest_pb2.ApiRequestPayload()
if params.session_token:
api_request_payload.encryptedSessionToken = utils.base64_url_decode(params.session_token)
Expand All @@ -818,7 +824,7 @@ def communicate_rest(params, request, endpoint, *, rs_type=None, payload_version
if isinstance(payload_version, int):
api_request_payload.apiVersion = payload_version

rs = rest_api.execute_rest(params.rest_context, endpoint, api_request_payload)
rs = rest_api.execute_rest(params.rest_context, endpoint, api_request_payload, timeout=timeout)
if isinstance(rs, bytes):
TTK.update_time_of_last_activity()
if rs_type:
Expand Down Expand Up @@ -850,7 +856,7 @@ def communicate(params, request, retry_on_throttle=True):
response_json = run_command(params, request)
if response_json['result'] != 'success':
if retry_on_throttle and response_json.get('result_code') == 'throttled':
logging.info('Throttled. sleeping for 10 seconds')
logging.debug('Throttled, retrying in 10 seconds')
time.sleep(10)
# Allow maximum 1 retry per call
return communicate(params, request, retry_on_throttle=False)
Expand Down
13 changes: 12 additions & 1 deletion keepercommander/commands/aram.py
Original file line number Diff line number Diff line change
Expand Up @@ -1941,10 +1941,21 @@ def parse_date(date_str):
in_shared_folder = kwargs.get('in_shared_folder')
node_id = get_node_id(params, enterprise_id)

# Pre-filter to specific user if --username is set
user_filter = None
username_arg = kwargs.get('username')
if username_arg:
user_lookup = {eu.get('username'): eu.get('enterprise_user_id')
for eu in params.enterprise.get('users', [])}
user_id = user_lookup.get(username_arg)
if user_id is None:
raise CommandError('aram', f'User "{username_arg}" not found in enterprise')
user_filter = {user_id}

get_sox_data_fn = get_compliance_data if exclude_deleted or in_shared_folder else get_prelim_data
sd_args = [params, node_id, enterprise_id, rebuild] if exclude_deleted or in_shared_folder \
else [params, enterprise_id, rebuild]
sd_kwargs = {'min_updated': period_min_ts}
sd_kwargs = {'min_updated': period_min_ts, 'user_filter': user_filter}
sd = get_sox_data_fn(*sd_args, **sd_kwargs)
AgingReportCommand.update_aging_data(params, sd, period_start_ts=period_min_ts, rebuild=rebuild)

Expand Down
Loading
Loading