From 1f5e2c6a35563aa65fbb134f8a52ece763bc2489 Mon Sep 17 00:00:00 2001 From: amangalampalli-ks Date: Mon, 10 Aug 2026 20:32:46 +0530 Subject: [PATCH 01/13] KC-1357 : Add Nested Share Folder (NSF) support for import (#2260) (#2276) * import support for nsf records and folders * added permissions for nsf import * added sample data * updated tests Co-authored-by: pvagare-ks --- keepercommander/importer/commands.py | 31 +- keepercommander/importer/imp_exp.py | 209 ++++++++--- keepercommander/importer/importer.py | 61 ++- keepercommander/importer/json/json.py | 7 +- keepercommander/importer/nsf_import.py | 452 ++++++++++++++++++++++ sample_data/import_nsf.txt | 499 +++++++++++++++++++++++++ sample_data/import_nsf_permissions.txt | 102 +++++ unit-tests/test_nsf_import.py | 233 ++++++++++++ 8 files changed, 1522 insertions(+), 72 deletions(-) create mode 100644 keepercommander/importer/nsf_import.py create mode 100644 sample_data/import_nsf.txt create mode 100644 sample_data/import_nsf_permissions.txt create mode 100644 unit-tests/test_nsf_import.py diff --git a/keepercommander/importer/commands.py b/keepercommander/importer/commands.py index 5fae4198a..fef8a3b83 100644 --- a/keepercommander/importer/commands.py +++ b/keepercommander/importer/commands.py @@ -61,6 +61,8 @@ def register_command_info(aliases, command_info): help='display records to be imported without importing them') import_parser.add_argument('-s', '--shared', dest='shared', action='store_true', help='import folders as Keeper shared folders') +import_parser.add_argument('--nsf', dest='use_nsf', action='store_true', + help='import folders and records into Nested Share Folders (works with any --format)') import_parser.add_argument('-p', '--permissions', dest='permissions', action='store', help='default shared folder permissions: manage (U)sers, manage (R)ecords, can (E)dit, can (S)hare, or (A)ll, (N)one') import_parser.add_argument('--update', dest='update_flag', action='store_true', @@ -68,7 +70,7 @@ def register_command_info(aliases, command_info): import_parser.add_argument('--no-shortcuts', dest='no_shortcuts', action='store_true', help='disable shortcut handling (only new records)') import_parser.add_argument('--users', dest='users_only', action='store_true', - help='update shared folder user permissions only') + help='update shared folder / NSF user permissions only') import_parser.add_argument('--record-type', dest='record_type', action='store', help='Import legacy records as record type') import_parser.add_argument('--login-type', '-l', dest='login_type', action='store_true', @@ -84,7 +86,8 @@ def register_command_info(aliases, command_info): import_parser.add_argument('--secret-ids', dest='secret_ids', action='store', help='Comma separated list of secret IDs to fetch (Thycotic)') import_parser.add_argument( - 'name', type=str, help='file name (json, csv, keepass, 1password), account name (lastpass), or URL (ManageEngine, Thycotic)' + 'name', type=str, + help='file name (json, csv , keepass, 1password), account name (lastpass), or URL (ManageEngine, Thycotic). ' ) import_parser.error = raise_parse_exception import_parser.exit = suppress_exit @@ -163,6 +166,9 @@ def register_command_info(aliases, command_info): To load the sample data: import --format=csv sample_data/import.csv + +Nested Share Folders (NSF): +import --format=csv --nsf sample_data/import_nsf.csv ''' json_instructions = '''JSON Import Instructions @@ -175,6 +181,23 @@ def register_command_info(aliases, command_info): To load the sample file into your vault, run this command: import --format=json sample_data/import.json.txt + +Nested Share Folders (NSF) — works with any --format (json, csv, keepass, …): +import --format=json --nsf sample_data/import_nsf.txt + +With --nsf, shared_folders[].permissions from JSON are granted on Nested +Share Folders after folders and records are created. Prefer NSF roles: + + "permissions": [ + { "name": "user@company.com", "role": "viewer" }, + { "name": "lead@company.com", "role": "content-manager" }, + { "name": "admin@company.com", "role": "full-manager" } + ] + +Accepted roles: requestor, viewer, share-manager, content-manager, +content-share-manager, full-manager. +Sample: sample_data/import_nsf_permissions.txt +Use --users --nsf to update NSF permissions only (no record import). ''' @@ -216,7 +239,9 @@ def execute(self, params, **kwargs): permissions = kwargs.get('permissions') or '' del kwargs['permissions'] - if kwargs.get('shared') is True and not permissions: + # Classic --shared prompts for default U/R/E/S permissions. + # NSF uses roles from the import file (and optional -p); do not prompt. + if kwargs.get('shared') is True and not kwargs.get('use_nsf') and not permissions: permissions = user_choice('Default shared folder permissions: manage (U)sers, manage (R)ecords, can (E)dit, can (S)hare, or (A)ll, (N)one', 'uresan', show_choice=False, multi_choice=True) if permissions: chars = set() diff --git a/keepercommander/importer/imp_exp.py b/keepercommander/importer/imp_exp.py index 912ab9ea4..1acb9ed0b 100644 --- a/keepercommander/importer/imp_exp.py +++ b/keepercommander/importer/imp_exp.py @@ -706,9 +706,41 @@ def import_user_permissions(params, logging.info("%d user(s) removed from shared folders", users_removed) +def _strip_path_prefix(records, folders, prefix): + # type: (List[ImportRecord], List[ImportSharedFolder], str) -> None + """Remove an import --folder prefix from record/shared-folder paths.""" + if not prefix: + return + prefix_l = prefix.lower() + delim = PathDelimiter + + def strip_one(path): + if not path: + return path + path_l = path.lower() + if path_l == prefix_l: + return '' + if path_l.startswith(prefix_l + delim): + return path[len(prefix) + len(delim):] + return path + + for rec in records or []: + if not rec.folders: + continue + for fol in rec.folders: + if fol.domain: + fol.domain = strip_one(fol.domain) + if fol.path: + fol.path = strip_one(fol.path) + for fol in folders or []: + if fol.path: + fol.path = strip_one(fol.path) + + def _import(params, file_format, filename, **kwargs): """Import records from one of a variety of sources.""" shared = kwargs.get('shared') or False + use_nsf = kwargs.get('use_nsf') or False import_users = kwargs.get('users_only') or False old_domain = kwargs.get('old_domain') new_domain = kwargs.get('new_domain') @@ -735,6 +767,9 @@ def _import(params, file_format, filename, **kwargs): filter_folder_lower = filter_folder.lower() if isinstance(filter_folder, str) else '' + # When --nsf is set, skip classic shared-folder domain splitting. + classic_shared = shared and not use_nsf + for x in importer.execute(filename, params=params, users_only=import_users, filter_folder=filter_folder, old_domain=old_domain, new_domain=new_domain, tmpdir=tmpdir, secret_ids=secret_ids, dry_run=dry_run): if isinstance(x, ImportRecord): @@ -759,11 +794,11 @@ def _import(params, file_format, filename, **kwargs): else: continue - if shared or import_into: + if classic_shared or import_into or use_nsf: if not x.folders: x.folders = [ImportFolder()] for f in x.folders: - if shared: + if classic_shared: d_comps = list(path_components(f.domain)) if f.domain else [] p_comps = list(path_components(f.path)) if f.path else [] if len(d_comps) > 0: @@ -789,7 +824,7 @@ def _import(params, file_format, filename, **kwargs): logging.info(ce.message) records.append(x) elif isinstance(x, ImportSharedFolder): - if shared: + if classic_shared: continue if filter_folder and not importer.support_folder_filter(): name = x.path.lower().lstrip('\\') @@ -803,18 +838,40 @@ def _import(params, file_format, filename, **kwargs): folders.append(x) - if import_users: - import_user_permissions(params, folders) - return - - sync_down.sync_down(params) - manage_users = kwargs.get('manage_users') or False manage_records = kwargs.get('manage_records') or False can_edit = kwargs.get('can_edit') or False can_share = kwargs.get('can_share') or False - if shared: + if import_users: + if use_nsf: + from .nsf_import import apply_nsf_folder_permissions, prepare_nsf_folders + sync_down.sync_down(params) + prepare_nsf_folders(params, folders, [], '') + apply_nsf_folder_permissions( + params, folders, manage_users, manage_records, can_edit, can_share) + else: + import_user_permissions(params, folders) + return + + sync_down.sync_down(params) + + nsf_base_parent = '' + if import_into: + from .nsf_import import resolve_nsf_folder + resolved_nsf = resolve_nsf_folder(params, import_into) + if resolved_nsf: + use_nsf = True + # If --folder was an NSF UID, treat it as parent and strip the UID prefix. + if import_into == resolved_nsf: + nsf_base_parent = resolved_nsf + _strip_path_prefix(records, folders, import_into) + + if use_nsf: + from .nsf_import import flatten_record_folder_paths + flatten_record_folder_paths(records) + + if classic_shared: sfol = set() for r in records: if r.folders: @@ -830,52 +887,58 @@ def _import(params, file_format, filename, **kwargs): sf.can_share = can_share folders.append(sf) - # shared folder mapping + # shared folder mapping (classic only) sf_map = {} # type: Dict[str, str] - for shared_folder_uid in params.shared_folder_cache: - folder = params.folder_cache.get(shared_folder_uid) - if not folder: - continue - if folder.parent_uid: - sf_path = get_folder_path(params, folder.parent_uid) - sf_path.strip(PathDelimiter) - else: - sf_path = '' - sf_name = folder.name.strip() - if ' - ' in sf_name: - sf_from_name = sf_name.replace(' - ', PathDelimiter) - sf_to_name = folder.name - if sf_path: - sf_from_name = sf_path + PathDelimiter + sf_from_name - sf_to_name = sf_path + PathDelimiter + sf_to_name - sf_map[sf_from_name.lower()] = sf_to_name - if len(sf_map) > 0: - sf_keys = list(sf_map.keys()) - sf_keys.sort() - for record in records: - if isinstance(record.folders, list) and len(record.folders) > 0: - for fol in record.folders: - path = fol.domain or '' - if fol.path: - if path: - path += PathDelimiter - path += fol.path - path_l = path.lower() - idx = bisect.bisect_right(sf_keys, path_l) - if 0 <= idx < len(sf_map) and path_l == sf_keys[idx]: - fol.domain = sf_map[path_l] - fol.path = '' - elif 0 < idx <= len(sf_map) and path_l.startswith(sf_keys[idx - 1]): - sf_name = sf_keys[idx - 1] - fol.domain = sf_map[sf_name] - fol.path = (path[len(sf_name):]).strip(PathDelimiter) - - folder_add = prepare_folder_add(params, folders, records, manage_users, manage_records, can_edit, can_share) - if folder_add: + if not use_nsf: + for shared_folder_uid in params.shared_folder_cache: + folder = params.folder_cache.get(shared_folder_uid) + if not folder: + continue + if folder.parent_uid: + sf_path = get_folder_path(params, folder.parent_uid) + sf_path.strip(PathDelimiter) + else: + sf_path = '' + sf_name = folder.name.strip() + if ' - ' in sf_name: + sf_from_name = sf_name.replace(' - ', PathDelimiter) + sf_to_name = folder.name + if sf_path: + sf_from_name = sf_path + PathDelimiter + sf_from_name + sf_to_name = sf_path + PathDelimiter + sf_to_name + sf_map[sf_from_name.lower()] = sf_to_name + if len(sf_map) > 0: + sf_keys = list(sf_map.keys()) + sf_keys.sort() + for record in records: + if isinstance(record.folders, list) and len(record.folders) > 0: + for fol in record.folders: + path = fol.domain or '' + if fol.path: + if path: + path += PathDelimiter + path += fol.path + path_l = path.lower() + idx = bisect.bisect_right(sf_keys, path_l) + if 0 <= idx < len(sf_map) and path_l == sf_keys[idx]: + fol.domain = sf_map[path_l] + fol.path = '' + elif 0 < idx <= len(sf_map) and path_l.startswith(sf_keys[idx - 1]): + sf_name = sf_keys[idx - 1] + fol.domain = sf_map[sf_name] + fol.path = (path[len(sf_name):]).strip(PathDelimiter) + + if use_nsf: + from .nsf_import import prepare_nsf_folders if not dry_run: - fol_rs, _ = execute_import_folder_record(params, folder_add, None) - _ = fol_rs - sync_down.sync_down(params) + prepare_nsf_folders(params, folders, records, nsf_base_parent) + else: + folder_add = prepare_folder_add(params, folders, records, manage_users, manage_records, can_edit, can_share) + if folder_add: + if not dry_run: + fol_rs, _ = execute_import_folder_record(params, folder_add, None) + _ = fol_rs + sync_down.sync_down(params) record_keys = {} audit_uids = [] @@ -884,6 +947,7 @@ def _import(params, file_format, filename, **kwargs): records_v2_to_update = [] # type: List[dict] records_v3_to_add = [] # type: List[record_pb2.RecordAdd] records_v3_to_update = [] # type: List[record_pb2.RecordUpdate] + nsf_records_to_add = [] # NSF vault/records/v3/add payloads import_uids = {} records_to_import, record_exists, external_lookup = prepare_record_add_or_update(update_flag, no_shortcuts, params, records) @@ -1028,6 +1092,23 @@ def _import(params, file_format, filename, **kwargs): if folder.type == BaseFolderNode.RootFolderType: folder_uid = '' + from .nsf_import import is_nsf_folder + if folder_uid and is_nsf_folder(params, folder_uid): + data = _construct_record_v3_data(import_record) + data_bytes = api.get_record_data_json_bytes(data) + if len(data_bytes) > RECORD_MAX_DATA_LEN: + logging.warning(RECORD_MAX_DATA_WARN.format(data['title'], len(data_bytes), RECORD_MAX_DATA_LEN)) + continue + from .nsf_import import build_nsf_record_add + try: + nsf_add = build_nsf_record_add(params, import_record, record_key, data) + except Exception as exc: + logging.warning('Skipping NSF record "%s": %s', import_record.title, exc) + continue + import_uids[import_record.uid] = {'ver': 'nsf', 'op': 'add'} + nsf_records_to_add.append(nsf_add) + continue + v3_add_rq = record_pb2.RecordAdd() v3_add_rq.record_uid = utils.base64_url_decode(import_record.uid) import_uids[import_record.uid] = {'ver': 'v3', 'op': 'add'} @@ -1088,6 +1169,9 @@ def _import(params, file_format, filename, **kwargs): if record_key and link_key: link.record_key = crypto.encrypt_aes_v2(link_key, record_key) + if nsf_records_to_add: + from .nsf_import import execute_nsf_records_add + execute_nsf_records_add(params, nsf_records_to_add) if records_v3_to_add: rec_rs = execute_records_add(params, records_v3_to_add) if records_v2_to_update: @@ -1095,7 +1179,11 @@ def _import(params, file_format, filename, **kwargs): if records_v3_to_update: rec_rs = execute_records_update(params, records_v3_to_update) - sync_down.sync_down(params) + if use_nsf or nsf_records_to_add: + from ..commands.pam_import.nsf_helpers import sync_down_preserving_nsf_keys + sync_down_preserving_nsf_keys(params) + else: + sync_down.sync_down(params) # update audit data if audit_uids and params.enterprise_ec_key: @@ -1161,7 +1249,7 @@ def _import(params, file_format, filename, **kwargs): if r.attachments: if r.uid in import_uids: ver = import_uids[r.uid]['ver'] - if ver == 'v3': + if ver == 'v3' or ver == 'nsf': v3_atts.append(r) else: for a in r.attachments: @@ -1207,6 +1295,11 @@ def _import(params, file_format, filename, **kwargs): if len(v3_atts) > 0: upload_v3_attachments(params, v3_atts) + if use_nsf and folders and not dry_run: + from .nsf_import import apply_nsf_folder_permissions + apply_nsf_folder_permissions( + params, folders, manage_users, manage_records, can_edit, can_share) + if hasattr(importer, 'cleanup') and callable(importer.cleanup): importer.cleanup() @@ -2224,6 +2317,10 @@ def prepare_record_link(params, records): folder_uid = fol.uid if fol else '' if folder_uid and folder_uid not in params.folder_cache: continue + from .nsf_import import is_nsf_folder + if folder_uid and is_nsf_folder(params, folder_uid): + # NSF multi-folder links use a different API; skip classic move/link. + continue if folder_uid in folder_ids: continue if len(folder_ids) > 0: diff --git a/keepercommander/importer/importer.py b/keepercommander/importer/importer.py index 3241c0371..9742b5119 100644 --- a/keepercommander/importer/importer.py +++ b/keepercommander/importer/importer.py @@ -103,6 +103,7 @@ def __init__(self): self.name = None self.manage_users = None self.manage_records = None + self.role = None # NSF role name: viewer, content-manager, full-manager, … class SharedFolder: @@ -398,21 +399,57 @@ class BaseFileImporter(BaseImporter, abc.ABC): def __init__(self): super(BaseFileImporter, self).__init__() - def execute(self, name, **kwargs): - # type: (str, ...) -> Iterable[Union[Record, SharedFolder, File]] + def _path_candidates(self, name): + # type: (str) -> List[str] + """Yield existing-path candidates for *name* (exact, +ext, sample .ext.txt).""" + path = os.path.expanduser(name) + candidates = [path] + ext = (self.extension() or '').lstrip('.') + if ext: + lower = path.lower() + ext_suffix = '.' + ext.lower() + ext_txt_suffix = ext_suffix + '.txt' + if lower.endswith(ext_txt_suffix): + pass + elif lower.endswith(ext_suffix): + # foo.json → also try foo.json.txt (sample_data convention) + candidates.append(path + '.txt') + else: + # foo → foo.json, foo.json.txt, foo.txt + candidates.append(path + '.' + ext) + candidates.append(path + '.' + ext + '.txt') + candidates.append(path + '.txt') + # Deduplicate while preserving order + seen = set() + for candidate in candidates: + if candidate in seen: + continue + seen.add(candidate) + yield candidate + + def resolve_file_path(self, name): + # type: (str) -> str + """Resolve an import file path, trying common extensions and sample suffixes.""" try: json.loads(name) - path = name - except ValueError as e: - path = os.path.expanduser(name) - if not os.path.isfile(path): - ext = self.extension() - if ext: - path = path + '.' + ext - - if not os.path.isfile(path): - raise CommandError('import', f'File \'{name}\' does not exist') + return name + except ValueError: + pass + + tried = [] + for candidate in self._path_candidates(name): + tried.append(candidate) + if os.path.isfile(candidate): + return candidate + hint = '' + ext = (self.extension() or '').lstrip('.') + if ext: + hint = f' (tried: {", ".join(tried[:4])}{"…" if len(tried) > 4 else ""})' + raise CommandError('import', f'File \'{name}\' does not exist{hint}') + def execute(self, name, **kwargs): + # type: (str, ...) -> Iterable[Union[Record, SharedFolder, File]] + path = self.resolve_file_path(name) yield from self.do_import(path, **kwargs) diff --git a/keepercommander/importer/json/json.py b/keepercommander/importer/json/json.py index b4c7a2484..567cf86a2 100644 --- a/keepercommander/importer/json/json.py +++ b/keepercommander/importer/json/json.py @@ -203,7 +203,9 @@ def do_import(self, filename, **kwargs): fol.manage_users = shf.get('manage_users') or False fol.can_edit = shf.get('can_edit') or False fol.can_share = shf.get('can_share') or False - if users_only and 'permissions' in shf: + # Always parse user/team permissions. Classic shared folders apply + # them with --users; NSF applies them during --nsf import. + if 'permissions' in shf: fol.permissions = [] permissions = shf['permissions'] if not isinstance(permissions, list): @@ -216,6 +218,9 @@ def do_import(self, filename, **kwargs): if p.uid or p.name: p.manage_records = perm.get('manage_records') or False p.manage_users = perm.get('manage_users') or False + role = perm.get('role') + if isinstance(role, str) and role.strip(): + p.role = role.strip() fol.permissions.append(p) yield fol diff --git a/keepercommander/importer/nsf_import.py b/keepercommander/importer/nsf_import.py new file mode 100644 index 000000000..a3d79fd90 --- /dev/null +++ b/keepercommander/importer/nsf_import.py @@ -0,0 +1,452 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' bool + """Return True when *folder_uid* is a Nested Share Folder.""" + if not folder_uid: + return False + if folder_uid in (getattr(params, 'nested_share_folders', None) or {}): + return True + subfolder = (getattr(params, 'subfolder_cache', None) or {}).get(folder_uid) or {} + if subfolder.get('source') == 'nested_share_folder': + return True + folder = (getattr(params, 'folder_cache', None) or {}).get(folder_uid) + return bool(folder and getattr(folder, 'type', None) == BaseFolderNode.NestedShareFolderType) + + +def resolve_nsf_folder(params, identifier): # type: (KeeperParams, Optional[str]) -> Optional[str] + """Resolve a folder name, path, or UID to an NSF folder UID.""" + if not identifier: + return None + from ..nested_share_folder.folder_api import resolve_folder_identifier + + for candidate in (identifier, identifier.replace('\\', '/')): + uid = resolve_folder_identifier(params, candidate) + if uid and is_nsf_folder(params, uid): + return uid + return None + + +def classic_perms_to_nsf_role(manage_users=False, manage_records=False, + can_edit=False, can_share=False): + """Map classic shared-folder booleans to an NSF role name.""" + if manage_users and manage_records: + return 'full-manager' + if manage_records and can_share: + return 'content-share-manager' + if manage_records or can_edit: + return 'content-manager' + if can_share: + return 'share-manager' + return 'viewer' + + +def flatten_record_folder_paths(records): # type: (List[ImportRecord]) -> None + """Collapse domain + path into a single NSF path on each record folder.""" + for record in records: + if not record.folders: + continue + for folder in record.folders: + comps = [] + if folder.domain: + comps.extend(path_components(folder.domain)) + if folder.path: + comps.extend(path_components(folder.path)) + folder.domain = '' + folder.path = PathDelimiter.join( + [c.replace(PathDelimiter, 2 * PathDelimiter) for c in comps] + ) if comps else '' + + +def find_nsf_child(params, folder_name, parent_uid): + # type: (KeeperParams, str, Optional[str]) -> Optional[str] + """Find an NSF child named *folder_name* under *parent_uid* (or vault root).""" + from ..commands.nested_share_folder.helpers import normalize_parent_uid + + nsf_folders = getattr(params, 'nested_share_folders', None) or {} + name_lower = (folder_name or '').lower() + looking_for_root = not parent_uid + for fuid, fobj in nsf_folders.items(): + if (fobj.get('name') or '').lower() != name_lower: + continue + raw_parent = fobj.get('parent_uid') or '' + normalized = normalize_parent_uid(raw_parent) + is_root_child = ( + normalized in ('', 'root') + or (raw_parent and raw_parent not in nsf_folders) + ) + if looking_for_root: + if is_root_child: + return fuid + elif raw_parent == parent_uid: + return fuid + return None + + +def _is_throttle_error(exc): # type: (BaseException) -> bool + if isinstance(exc, KeeperApiError) and str(getattr(exc, 'result_code', '')).lower() in ( + 'throttled', '429', 'too_many_requests'): + return True + text = str(getattr(exc, 'message', None) or exc).lower() + return 'throttl' in text or 'too many' in text or 'rate limit' in text + + +def _call_with_throttle_retry(description, fn, *args, **kwargs): + """Retry *fn* with progressive backoff when the API returns throttled.""" + wait = _THROTTLE_BASE_WAIT + for attempt in range(1, _THROTTLE_MAX_RETRIES + 1): + try: + return fn(*args, **kwargs) + except Exception as exc: + if not _is_throttle_error(exc) or attempt >= _THROTTLE_MAX_RETRIES: + raise + logging.warning( + '%s throttled (attempt %d/%d); waiting %.0fs before retry', + description, attempt, _THROTTLE_MAX_RETRIES, wait, + ) + time.sleep(wait) + wait *= _THROTTLE_MULTIPLIER + + +def create_nsf_folder(params, name, parent_uid=''): + # type: (KeeperParams, str, Optional[str]) -> str + """Create one NSF folder and seed local caches. Returns folder UID.""" + results = create_nsf_folders_batch(params, [(name, parent_uid or '')]) + if not results: + raise CommandError('import', f'Failed to create NSF folder "{name}"') + return results[0] + + +def create_nsf_folders_batch(params, folders): + # type: (KeeperParams, List[Tuple[str, str]]) -> List[str] + """Create many NSF folders that share the same depth (parents already exist). + + *folders* is a list of ``(name, parent_uid)`` pairs. Returns UIDs in the + same order. Uses ``vault/folders/v3/add`` batches of up to 100 with + throttle retries. + """ + from ..nested_share_folder.folder_api import create_folders_batch_v3 + from ..commands.pam_import.nsf_helpers import seed_nsf_folder_cache + + if not folders: + return [] + + created_uids = [] # type: List[str] + for start in range(0, len(folders), NSF_FOLDER_BATCH): + chunk = folders[start:start + NSF_FOLDER_BATCH] + specs = [ + { + 'name': name, + 'parent_uid': parent_uid or None, + 'inherit_permissions': True, + } + for name, parent_uid in chunk + ] + results = _call_with_throttle_retry( + 'NSF folder create', + create_folders_batch_v3, + params, + specs, + ) + if len(results) != len(chunk): + raise CommandError( + 'import', + f'NSF folder batch returned {len(results)} results for {len(chunk)} folders', + ) + for (name, parent_uid), result in zip(chunk, results): + if not isinstance(result, dict) or result.get('success') is False: + message = (result or {}).get('message') if isinstance(result, dict) else None + raise CommandError( + 'import', + message or f'Failed to create NSF folder "{name}"', + ) + folder_uid = result['folder_uid'] + seed_nsf_folder_cache( + params, folder_uid, name, parent_uid or None, + result.get('folder_key_unencrypted'), + ) + created_uids.append(folder_uid) + return created_uids + + +def ensure_nsf_path(params, comps, parent_uid=''): + # type: (KeeperParams, Iterable[str], Optional[str]) -> str + """Ensure path components exist under *parent_uid*; return leaf folder UID.""" + current = parent_uid or '' + for comp in comps: + if not comp: + continue + existing = find_nsf_child(params, comp, current) + if existing: + current = existing + continue + current = create_nsf_folder(params, comp, current) + return current + + +def _folder_comp_lists(folders, records): + # type: (List[ImportSharedFolder], List[ImportRecord]) -> List[Tuple[object, List[str]]] + """Return ``(target, component_list)`` pairs for every folder to resolve.""" + targets = [] # type: List[Tuple[object, List[str]]] + for fol in folders or []: + comps = [c for c in path_components(fol.path or '') if c] + if comps: + targets.append((fol, comps)) + for rec in records or []: + if not rec.folders: + continue + for fol in rec.folders: + comps = [] + if fol.domain: + comps.extend(path_components(fol.domain)) + if fol.path: + comps.extend(path_components(fol.path)) + comps = [c for c in comps if c] + targets.append((fol, comps)) + return targets + + +def prepare_nsf_folders(params, folders, records, base_parent_uid=''): + # type: (KeeperParams, List[ImportSharedFolder], List[ImportRecord], str) -> int + """Create NSF folder trees for import shared folders and record folders. + + Builds the full tree first, then creates missing folders **by depth** in + batches of up to 100 (``vault/folders/v3/add``), with throttle retries. + Sets ``uid`` on each resolved ``ImportSharedFolder`` / ``ImportFolder``. + Returns the number of folders created. + """ + before = set((getattr(params, 'nested_share_folders', None) or {}).keys()) + targets = _folder_comp_lists(folders, records) + + # path_tuple -> folder_uid + uid_by_path = {} # type: Dict[Tuple[str, ...], str] + if base_parent_uid: + uid_by_path[()] = base_parent_uid + + # Resolve / create level by level so parent keys exist for children. + max_depth = max((len(comps) for _, comps in targets), default=0) + for depth in range(1, max_depth + 1): + pending = [] # type: List[Tuple[Tuple[str, ...], str, str]] + seen = set() # type: set + for _, comps in targets: + if len(comps) < depth: + continue + path_tuple = tuple(comps[:depth]) + if path_tuple in uid_by_path or path_tuple in seen: + continue + parent_tuple = path_tuple[:-1] + if parent_tuple not in uid_by_path and parent_tuple != (): + # Parent should have been resolved at previous depth. + continue + parent_uid = uid_by_path.get(parent_tuple, base_parent_uid or '') + name = path_tuple[-1] + existing = find_nsf_child(params, name, parent_uid) + if existing: + uid_by_path[path_tuple] = existing + continue + seen.add(path_tuple) + pending.append((path_tuple, name, parent_uid)) + + if not pending: + continue + + batch_input = [(name, parent_uid) for _, name, parent_uid in pending] + created_uids = create_nsf_folders_batch(params, batch_input) + for (path_tuple, _name, _parent_uid), folder_uid in zip(pending, created_uids): + uid_by_path[path_tuple] = folder_uid + + # Assign leaf UIDs back onto import objects. + for target, comps in targets: + if not comps: + if base_parent_uid: + target.uid = base_parent_uid + else: + target.uid = '' + continue + path_tuple = tuple(comps) + target.uid = uid_by_path.get(path_tuple, '') + + after = set((getattr(params, 'nested_share_folders', None) or {}).keys()) + created = len(after - before) + if created: + logging.info('Created %d Nested Share Folder(s)', created) + return created + + +def build_nsf_record_add(params, import_record, record_key, data): + """Build a ``vault/records/v3/add`` RecordAdd for an NSF folder.""" + from ..nested_share_folder.common import get_folder_key + from ..nested_share_folder.record_api import create_record_data_v3 + + folder_uid = '' + if import_record.folders: + folder_uid = import_record.folders[0].uid or '' + if not folder_uid or not is_nsf_folder(params, folder_uid): + raise CommandError('import', f'NSF folder not found for record "{import_record.title}"') + + folder_key = get_folder_key(params, folder_uid, raise_on_missing=True) + return create_record_data_v3( + record_uid=import_record.uid, + record_key=record_key, + data=data, + folder_uid=folder_uid, + folder_key=folder_key, + data_key=params.data_key, + client_modified_time=utils.current_milli_time(), + ) + + +def execute_nsf_records_add(params, record_adds): + # type: (KeeperParams, list) -> list + """Submit NSF record adds in batches of up to 1000 with throttle retries.""" + from ..nested_share_folder.record_api import record_add_v3 + + results = [] + for start in range(0, len(record_adds), NSF_RECORD_BATCH): + batch = record_adds[start:start + NSF_RECORD_BATCH] + try: + rs = _call_with_throttle_retry( + 'NSF record add', + record_add_v3, + params, + batch, + ) + results.extend(list(rs.records)) + except Exception as exc: + logging.warning('NSF record add batch failed: %s', exc) + for ra in batch: + uid = utils.base64_url_encode(ra.recordUid) + logging.warning('Failed to create NSF record %s', uid) + return results + + +def apply_nsf_folder_permissions(params, folders, manage_users=False, manage_records=False, + can_edit=False, can_share=False): + # type: (KeeperParams, List[ImportSharedFolder], bool, bool, bool, bool) -> None + """Grant NSF folder access from ImportSharedFolder.permissions. + + Each permission may specify an NSF ``role`` (viewer, content-manager, + full-manager, …). When ``role`` is absent, classic manage_users / + manage_records flags are mapped via ``classic_perms_to_nsf_role``. + """ + from ..nested_share_folder.folder_api import grant_folder_access_v3 + from ..nested_share_folder.permissions import resolve_role_name + + default_role = classic_perms_to_nsf_role( + manage_users, manage_records, can_edit, can_share) + + granted = 0 + for fol in folders or []: + if not fol.uid or not is_nsf_folder(params, fol.uid): + if not fol.uid and fol.path: + comps = list(path_components(fol.path)) + parent = '' + for comp in comps: + parent = find_nsf_child(params, comp, parent) or '' + if not parent: + break + fol.uid = parent + if not fol.uid or not is_nsf_folder(params, fol.uid): + continue + + folder_role = classic_perms_to_nsf_role( + fol.manage_users if fol.manage_users is not None else manage_users, + fol.manage_records if fol.manage_records is not None else manage_records, + fol.can_edit if fol.can_edit is not None else can_edit, + fol.can_share if fol.can_share is not None else can_share, + ) + + for perm in fol.permissions or []: # type: ImportPermission + explicit_role = (getattr(perm, 'role', None) or '').strip() + if explicit_role: + try: + resolve_role_name(explicit_role) + except ValueError as exc: + logging.warning( + 'NSF permission for "%s" on "%s": %s', + perm.name or perm.uid, fol.path or fol.uid, exc) + continue + role = explicit_role.strip().lower().replace('_', '-') + if role == 'shared-manager': + role = 'share-manager' + elif role == 'contributor': + role = 'requestor' + else: + role = classic_perms_to_nsf_role( + perm.manage_users if perm.manage_users is not None else False, + perm.manage_records if perm.manage_records is not None else False, + ) + if role == 'viewer' and (manage_users or manage_records or can_edit or can_share): + role = folder_role if folder_role != 'viewer' else default_role + + name = perm.name or '' + uid = perm.uid or '' + recipient = name or uid + if not recipient: + continue + if uid and '@' not in uid and (not name or '@' not in name): + as_team = True + recipient = uid + elif name and '@' in name: + as_team = False + recipient = name + elif name: + as_team = False + recipient = name + else: + as_team = False + + try: + result = _call_with_throttle_retry( + 'NSF folder permission', + grant_folder_access_v3, + params, fol.uid, recipient, role=role, as_team=as_team, + ) + if result.get('success'): + granted += 1 + else: + logging.warning( + 'NSF permission for "%s" on "%s": %s', + recipient, fol.path or fol.uid, result.get('message')) + except Exception as exc: + logging.warning( + 'NSF permission for "%s" on "%s" failed: %s', + recipient, fol.path or fol.uid, exc) + + if granted: + logging.info('Granted %d Nested Share Folder permission(s)', granted) diff --git a/sample_data/import_nsf.txt b/sample_data/import_nsf.txt new file mode 100644 index 000000000..01b75dedb --- /dev/null +++ b/sample_data/import_nsf.txt @@ -0,0 +1,499 @@ +{ + "shared_folders": [ + { + "path": "NSF All Types\\Credentials", + "manage_users": true, + "manage_records": true, + "can_edit": true, + "can_share": true, + "permissions": [ + { + "name": "user@mycompany.com", + "manage_users": false, + "manage_records": true + }, + { + "name": "admin@mycompany.com", + "manage_users": true, + "manage_records": true + } + ] + }, + { + "path": "NSF All Types\\Personal", + "manage_users": false, + "manage_records": true, + "can_edit": true, + "can_share": false, + "permissions": [ + { + "name": "user@mycompany.com", + "manage_users": false, + "manage_records": false + } + ] + }, + { + "path": "NSF All Types\\Infrastructure\\Servers", + "manage_users": true, + "manage_records": true, + "can_edit": true, + "can_share": true + }, + { + "path": "NSF All Types\\Infrastructure\\Databases", + "manage_users": true, + "manage_records": true, + "can_edit": true, + "can_share": false + } + ], + "records": [ + { + "uid": "AddrRec00000000000001", + "title": "Sample - Address", + "$type": "address", + "notes": "Home address sample", + "custom_fields": { + "$address": { + "street1": "100 Main Street", + "street2": "Apt 4B", + "city": "San Francisco", + "state": "CA", + "zip": "94105", + "country": "US" + } + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Identity" + } + ] + }, + { + "uid": "CardRec00000000000001", + "title": "Sample - Payment Card", + "$type": "bankCard", + "notes": "Visa demo card", + "custom_fields": { + "$paymentCard": { + "cardNumber": "4111111111111111", + "cardExpirationDate": "12/2030", + "cardSecurityCode": "123" + }, + "$text:cardholderName": "Jane Doe", + "$pinCode": "4321", + "$addressRef": "AddrRec00000000000001" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Finance" + } + ] + }, + { + "title": "Sample - Bank Account", + "$type": "bankAccount", + "login": "jdoe", + "password": "BankPass!234", + "login_url": "https://bank.example.com", + "notes": "Checking account sample", + "custom_fields": { + "$bankAccount": { + "accountType": "Checking", + "routingNumber": "021000021", + "accountNumber": "1234567890", + "otherType": "" + }, + "$name": { + "first": "Jane", + "middle": "Q", + "last": "Doe" + }, + "$cardRef": "CardRec00000000000001" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Finance" + } + ] + }, + { + "title": "Sample - Login", + "$type": "login", + "login": "jane.doe@example.com", + "password": "LoginPass!234", + "login_url": "https://accounts.example.com", + "notes": "Standard login with TOTP and security question", + "custom_fields": { + "$oneTimeCode": "otpauth://totp/Example:jane.doe@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=SHA1&digits=6&period=30", + "$securityQuestion": [ + { + "question": "What is your favorite color?", + "answer": "Blue" + }, + { + "question": "Pet name?", + "answer": "Fluffy" + } + ], + "Department": "Engineering", + "Employee ID": "E-10042" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Credentials", + "folder": "Websites", + "can_edit": true, + "can_share": true + } + ] + }, + { + "title": "Sample - General", + "$type": "general", + "login": "general-user", + "password": "GeneralPass!234", + "login_url": "https://general.example.com", + "notes": "Catch-all / general purpose record", + "custom_fields": { + "$text:label": "Misc info", + "Freeform Field": "Any custom value" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Credentials", + "folder": "Misc" + } + ] + }, + { + "title": "Sample - Encrypted Notes", + "$type": "encryptedNotes", + "notes": "Visible notes field", + "custom_fields": { + "$note": "This is a secured / masked note body.\nLine 2 of the secure note.", + "$date": 1719792000000 + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Notes" + } + ] + }, + { + "title": "Sample - Contact", + "$type": "contact", + "notes": "Business contact", + "custom_fields": { + "$name": { + "first": "John", + "middle": "A", + "last": "Smith" + }, + "$text:company": "Acme Corp", + "$email": "john.smith@acme.example.com", + "$phone": [ + { + "region": "+1", + "number": "415-555-0100", + "ext": "200", + "type": "Work" + }, + { + "region": "+1", + "number": "415-555-0199", + "type": "Mobile" + } + ], + "$addressRef": "AddrRec00000000000001" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Contacts" + } + ] + }, + { + "title": "Sample - Birth Certificate", + "$type": "birthCertificate", + "notes": "Birth certificate sample", + "custom_fields": { + "$name": { + "first": "Jane", + "middle": "Q", + "last": "Doe" + }, + "$birthDate": 631152000000 + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Identity" + } + ] + }, + { + "title": "Sample - Driver License", + "$type": "driverLicense", + "notes": "Driver license sample", + "custom_fields": { + "$accountNumber": "D1234567", + "$name": { + "first": "Jane", + "middle": "Q", + "last": "Doe" + }, + "$birthDate": 631152000000, + "$expirationDate": 1893456000000, + "$addressRef": "AddrRec00000000000001" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Identity" + } + ] + }, + { + "title": "Sample - Passport", + "$type": "passport", + "password": "PassportPin!99", + "notes": "Passport sample", + "custom_fields": { + "$accountNumber": "P123456789", + "$name": { + "first": "Jane", + "middle": "Q", + "last": "Doe" + }, + "$birthDate": 631152000000, + "$expirationDate": 1924905600000, + "$date:dateIssued": 1609459200000, + "$addressRef": "AddrRec00000000000001" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Identity" + } + ] + }, + { + "title": "Sample - SSN / Identity Card", + "$type": "ssnCard", + "notes": "Identity card sample", + "custom_fields": { + "$accountNumber": "123-45-6789", + "$name": { + "first": "Jane", + "middle": "Q", + "last": "Doe" + } + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Identity" + } + ] + }, + { + "title": "Sample - Health Insurance", + "$type": "healthInsurance", + "login": "member123", + "password": "HealthPass!234", + "login_url": "https://health.example.com", + "notes": "Health insurance sample", + "custom_fields": { + "$accountNumber": "HIN-998877", + "$name": { + "first": "Jane", + "middle": "Q", + "last": "Doe" + } + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Insurance" + } + ] + }, + { + "title": "Sample - Membership", + "$type": "membership", + "password": "MemberPass!234", + "notes": "Gym / club membership", + "custom_fields": { + "$accountNumber": "MEM-554433", + "$name": { + "first": "Jane", + "middle": "Q", + "last": "Doe" + } + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Memberships" + } + ] + }, + { + "title": "Sample - Software License", + "$type": "softwareLicense", + "notes": "Software license key sample", + "custom_fields": { + "$licenseNumber": "AAAA-BBBB-CCCC-DDDD-EEEE", + "$expirationDate": 1893456000000, + "$date:dateActive": 1704067200000, + "$text:product": "Example Pro Suite", + "$text:seats": "10" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Credentials", + "folder": "Licenses" + } + ] + }, + { + "title": "Sample - Server Credentials", + "$type": "serverCredentials", + "login": "root", + "password": "ServerPass!234", + "notes": "Linux / Windows server", + "custom_fields": { + "$host": { + "hostName": "10.0.1.30", + "port": "22" + }, + "$text:platformId": "UnixSSH", + "$oneTimeCode": "otpauth://totp/Server:root?secret=JBSWY3DPEHPK3PXP&issuer=Server&algorithm=SHA1&digits=6&period=30" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Infrastructure\\Servers", + "folder": "Unix", + "can_edit": true, + "can_share": false + } + ] + }, + { + "title": "Sample - Database Credentials", + "$type": "databaseCredentials", + "login": "dbadmin", + "password": "DbPass!234", + "notes": "PostgreSQL sample", + "custom_fields": { + "$text:type": "postgresql", + "$host": { + "hostName": "db.example.com", + "port": "5432" + } + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Infrastructure\\Databases", + "can_edit": true, + "can_share": false + } + ] + }, + { + "title": "Sample - SSH Keys", + "$type": "sshKeys", + "login": "deploy", + "password": "KeyPassphrase!234", + "notes": "SSH key pair sample", + "custom_fields": { + "$host": { + "hostName": "git.example.com", + "port": "22" + }, + "$keyPair": { + "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExamplePublicKeyOnlyNotReal deploy@example.com", + "privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nEXAMPLE_PRIVATE_KEY_PLACEHOLDER_NOT_REAL\n-----END OPENSSH PRIVATE KEY-----" + } + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Infrastructure\\Servers", + "folder": "SSH Keys", + "can_edit": true, + "can_share": false + } + ] + }, + { + "title": "Sample - WiFi Credentials", + "$type": "wifiCredentials", + "password": "WifiPass!234", + "notes": "Office WiFi", + "custom_fields": { + "$text:ssid": "Office-Guest", + "$text:networkSecurityType": "WPA2", + "$text:hiddenNetwork": "false" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Credentials", + "folder": "Networks" + } + ] + }, + { + "title": "Sample - File Attachment Placeholder", + "$type": "file", + "notes": "File records are normally created when uploading attachments. This is a placeholder title/notes-only sample.", + "custom_fields": {}, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Files" + } + ] + }, + { + "title": "Sample - Photo Placeholder", + "$type": "photo", + "notes": "Photo records are normally created when uploading images. Placeholder sample only.", + "custom_fields": {}, + "folders": [ + { + "shared_folder": "NSF All Types\\Personal", + "folder": "Photos" + } + ] + }, + { + "title": "Sample - Multi Folder Placement", + "$type": "login", + "login": "shared.user", + "password": "MultiFolder!234", + "login_url": "https://shared.example.com", + "notes": "Same record path appears under Credentials with NSF nesting from shared_folder + folder", + "custom_fields": { + "Owner Team": "Platform" + }, + "folders": [ + { + "shared_folder": "NSF All Types\\Credentials", + "folder": "Shared Apps", + "can_edit": true, + "can_share": true + }, + { + "folder": "NSF All Types\\Personal\\Bookmarks" + } + ] + } + ] +} diff --git a/sample_data/import_nsf_permissions.txt b/sample_data/import_nsf_permissions.txt new file mode 100644 index 000000000..322ccaabe --- /dev/null +++ b/sample_data/import_nsf_permissions.txt @@ -0,0 +1,102 @@ +{ + "shared_folders": [ + { + "path": "NSF Roles Demo\\Engineering", + "permissions": [ + { + "name": "user@mycompany.com", + "role": "viewer" + }, + { + "name": "user1@mycompany.com", + "role": "share-manager" + }, + { + "name": "user2@mycompany.com", + "role": "share-manager" + }, + { + "name": "user3@mycompany.com", + "role": "content-manager" + }, + { + "name": "user4@mycompany.com", + "role": "content-share-manager" + }, + { + "name": "user5@mycompany.com", + "role": "full-manager" + }, + { + "uid": "REPLACE_WITH_TEAM_UID", + "name": "Engineering Team", + "role": "content-manager" + } + ] + }, + { + "path": "NSF Roles Demo\\Finance", + "permissions": [ + { + "name": "user6@mycompany.com", + "role": "viewer" + }, + { + "name": "user7@mycompany.com", + "role": "full-manager" + } + ] + }, + { + "path": "NSF Roles Demo\\Contractors", + "permissions": [ + { + "name": "user8@mycompany.com", + "role": "requestor" + }, + { + "name": "user9@mycompany.com", + "role": "share-manager" + } + ] + } + ], + "records": [ + { + "title": "GitHub — Engineering", + "login": "eng-bot@mycompany.com", + "password": "EngSamp1e!Pass", + "login_url": "https://github.com/mycompany", + "notes": "Shared engineering org credentials", + "folders": [ + { + "folder": "NSF Roles Demo\\Engineering" + } + ] + }, + { + "title": "AWS Console — Finance", + "login": "finance-ops@mycompany.com", + "password": "FinSamp1e!Pass", + "login_url": "https://console.aws.amazon.com", + "notes": "Finance AWS account", + "folders": [ + { + "folder": "NSF Roles Demo\\Finance" + } + ] + }, + { + "title": "Partner Portal", + "login": "contractor@partner.com", + "password": "PartnerSamp1e!Pass", + "login_url": "https://partners.example.com", + "notes": "Limited contractor access folder", + "folders": [ + { + "folder": "NSF Roles Demo\\Contractors" + } + ] + } + ] +} diff --git a/unit-tests/test_nsf_import.py b/unit-tests/test_nsf_import.py new file mode 100644 index 000000000..520686a2b --- /dev/null +++ b/unit-tests/test_nsf_import.py @@ -0,0 +1,233 @@ +import json +import os +import tempfile +from unittest import TestCase, mock + +from keepercommander.error import KeeperApiError +from keepercommander.importer import nsf_import +from keepercommander.importer.commands import RecordImportCommand, import_parser +from keepercommander.importer.csv.csv import KeeperCsvImporter +from keepercommander.importer.importer import Folder, Record, SharedFolder, Permission +from keepercommander.importer.json.json import KeeperJsonImporter +from keepercommander.subfolder import NestedShareFolderNode + +SAMPLE = os.path.join(os.path.dirname(__file__), '..', 'sample_data') +GRANT = 'keepercommander.nested_share_folder.folder_api.grant_folder_access_v3' +CREATE = 'keepercommander.importer.nsf_import.create_nsf_folders_batch' + + +def _params(folders=None): + p = mock.MagicMock() + p.nested_share_folders = folders or {} + p.subfolder_cache = {} + p.folder_cache = {} + p.enforcements = None + return p + + +def _record(path): + rec = Record() + fol = Folder() + fol.path = path + rec.folders = [fol] + return rec, fol + + +def _perm(name=None, uid=None, role=None, manage_records=False, manage_users=False): + p = Permission() + p.name = name + p.uid = uid + p.role = role + p.manage_records = manage_records + p.manage_users = manage_users + return p + + +class TestNsfImport(TestCase): + def test_classic_perms_to_nsf_role(self): + cases = [ + ((True, True), 'full-manager'), + ((False, True), 'content-manager'), + ((False, False), 'viewer'), + ] + for args, role in cases: + self.assertEqual(nsf_import.classic_perms_to_nsf_role(*args), role) + self.assertEqual( + nsf_import.classic_perms_to_nsf_role(False, True, can_share=True), + 'content-share-manager') + self.assertEqual( + nsf_import.classic_perms_to_nsf_role(False, False, can_share=True), + 'share-manager') + + def test_flatten_and_find(self): + rec, fol = _record('') + fol.domain = 'Shared' + fol.path = 'Team\\Apps' + nsf_import.flatten_record_folder_paths([rec]) + self.assertEqual((fol.domain, fol.path), ('', 'Shared\\Team\\Apps')) + + params = _params({ + 'root1': {'name': 'Migration', 'parent_uid': ''}, + 'child1': {'name': 'Apps', 'parent_uid': 'root1'}, + }) + self.assertEqual(nsf_import.find_nsf_child(params, 'Migration', ''), 'root1') + self.assertEqual(nsf_import.find_nsf_child(params, 'Apps', 'root1'), 'child1') + self.assertIsNone(nsf_import.find_nsf_child(params, 'Missing', 'root1')) + + def test_is_nsf_folder(self): + params = _params({'nsf1': {'name': 'Root'}}) + params.subfolder_cache = {'nsf2': {'source': 'nested_share_folder'}} + node = NestedShareFolderNode() + node.uid = 'nsf3' + params.folder_cache = {'nsf3': node} + self.assertTrue(nsf_import.is_nsf_folder(params, 'nsf1')) + self.assertTrue(nsf_import.is_nsf_folder(params, 'nsf2')) + self.assertTrue(nsf_import.is_nsf_folder(params, 'nsf3')) + self.assertFalse(nsf_import.is_nsf_folder(params, 'missing')) + + def test_prepare_nsf_folders(self): + params = _params({ + 'root1': {'name': 'Migration', 'parent_uid': ''}, + 'child1': {'name': 'Apps', 'parent_uid': 'root1'}, + }) + rec, fol = _record('Migration\\Apps') + self.assertEqual(nsf_import.prepare_nsf_folders(params, [], [rec]), 0) + self.assertEqual(fol.uid, 'child1') + + rec, fol = _record('Migration\\New') + with mock.patch(CREATE, return_value=['new1']) as m: + nsf_import.prepare_nsf_folders(params, [], [rec]) + m.assert_called_once_with(params, [('New', 'root1')]) + self.assertEqual(fol.uid, 'new1') + + sf = SharedFolder() + sf.path = 'ProjectA' + with mock.patch(CREATE, return_value=['proj1']) as m: + nsf_import.prepare_nsf_folders(_params({'base': {'name': 'Base', 'parent_uid': ''}}), + [sf], [], 'base') + m.assert_called_once_with(mock.ANY, [('ProjectA', 'base')]) + self.assertEqual(sf.uid, 'proj1') + + def test_prepare_nsf_folders_batches_same_depth(self): + params = _params({'root': {'name': 'Root', 'parent_uid': ''}}) + rec_a, fol_a = _record('Root\\A') + rec_b, fol_b = _record('Root\\B') + with mock.patch(CREATE, return_value=['uidA', 'uidB']) as m: + nsf_import.prepare_nsf_folders(params, [], [rec_a, rec_b]) + m.assert_called_once_with(params, [('A', 'root'), ('B', 'root')]) + self.assertEqual((fol_a.uid, fol_b.uid), ('uidA', 'uidB')) + + def test_throttle_retry(self): + n = {'c': 0} + + def flaky(): + n['c'] += 1 + if n['c'] < 3: + raise KeeperApiError('throttled', 'throttled') + return 'ok' + + with mock.patch('keepercommander.importer.nsf_import.time.sleep') as sleep: + self.assertEqual(nsf_import._call_with_throttle_retry('t', flaky), 'ok') + self.assertEqual((n['c'], sleep.call_count), (3, 2)) + + def test_apply_permissions_classic_and_role(self): + params = _params({'f1': {'name': 'Team', 'parent_uid': ''}}) + sf = SharedFolder() + sf.uid = 'f1' + sf.path = 'Team' + sf.permissions = [ + _perm(name='user@example.com', manage_records=True), + _perm(name='viewer@example.com', role='viewer'), + _perm(name='admin@example.com', role='full-manager'), + _perm(name='editor@example.com', role='content_manager'), + _perm(name='bad@example.com', role='superuser'), + ] + with mock.patch(GRANT, return_value={'success': True}) as grant: + nsf_import.apply_nsf_folder_permissions(params, [sf]) + roles = [c.kwargs.get('role') for c in grant.call_args_list] + self.assertEqual(roles, ['content-manager', 'viewer', 'full-manager', 'content-manager']) + self.assertEqual(grant.call_args_list[0].args[2], 'user@example.com') + + def test_json_loads_permissions_and_roles(self): + payload = { + 'shared_folders': [{ + 'path': 'NSF\\Team', + 'permissions': [ + {'name': 'u@ex.com', 'manage_records': True}, + {'name': 'a@ex.com', 'role': 'full-manager'}, + {'uid': 'teamUid12345678901234', 'role': 'viewer'}, + ], + }], + 'records': [{'title': 'R', 'login': 'u', 'password': 'p', + 'folders': [{'folder': 'NSF\\Team'}]}], + } + with tempfile.NamedTemporaryFile('w', suffix='.txt', delete=False, encoding='utf-8') as tf: + json.dump(payload, tf) + path = tf.name + try: + items = list(KeeperJsonImporter().do_import(path)) + finally: + os.unlink(path) + + folders = [x for x in items if isinstance(x, SharedFolder)] + self.assertEqual(len(folders), 1) + perms = folders[0].permissions + self.assertEqual(len(perms), 3) + self.assertEqual(perms[0].name, 'u@ex.com') + self.assertTrue(perms[0].manage_records) + self.assertEqual(perms[1].role, 'full-manager') + self.assertEqual(perms[2].uid, 'teamUid12345678901234') + + def test_sample_files(self): + for name, check in ( + ('import_nsf.txt', lambda fs: any(p.name == 'user@mycompany.com' + for f in fs for p in (f.permissions or []))), + ('import_nsf_permissions.txt', lambda fs: { + p.role for f in fs for p in (f.permissions or []) if p.role + } >= {'viewer', 'full-manager', 'content-manager'}), + ): + path = os.path.join(SAMPLE, name) + if not os.path.isfile(path): + self.skipTest(f'{name} missing') + folders = [x for x in KeeperJsonImporter().do_import(path) if isinstance(x, SharedFolder)] + self.assertTrue(check(folders), name) + + def test_cli_nsf_skips_classic_prompt(self): + cmd = RecordImportCommand() + with mock.patch('keepercommander.importer.commands.user_choice') as choice, \ + mock.patch('keepercommander.importer.commands.imp_exp._import') as imp: + cmd.execute(_params(), format='json', name='sample_data/import_nsf.txt', + use_nsf=True, users_only=True) + choice.assert_not_called() + self.assertTrue(all(not imp.call_args.kwargs.get(k) + for k in ('manage_users', 'manage_records', 'can_edit', 'can_share'))) + + # classic --shared still prompts (name is unused; import is mocked) + choice.reset_mock() + choice.return_value = 'a' + cmd.execute(_params(), format='json', name='sample_data/import_nsf.txt', shared=True) + choice.assert_called_once() + self.assertTrue(all(imp.call_args.kwargs.get(k) + for k in ('manage_users', 'manage_records', 'can_edit', 'can_share'))) + + def test_parser_and_path_resolve(self): + self.assertTrue(any('--nsf' in (a.option_strings or []) for a in import_parser._actions)) + + nsf_txt = os.path.join(SAMPLE, 'import_nsf.txt') + if not os.path.isfile(nsf_txt): + self.skipTest('sample_data/import_nsf.txt missing') + + json_imp = KeeperJsonImporter() + # stem → import_nsf.txt + self.assertTrue( + json_imp.resolve_file_path(os.path.join(SAMPLE, 'import_nsf')).endswith('import_nsf.txt')) + # exact .txt path + self.assertEqual( + os.path.abspath(json_imp.resolve_file_path(nsf_txt)), + os.path.abspath(nsf_txt)) + + csv_path = os.path.join(SAMPLE, 'import_nsf.csv') + if os.path.isfile(csv_path): + self.assertEqual( + os.path.abspath(KeeperCsvImporter().resolve_file_path(csv_path)), + os.path.abspath(csv_path)) From fb34254974f304fab5c54c45ab46b6a8aef412f3 Mon Sep 17 00:00:00 2001 From: sshrushanth-ks Date: Mon, 10 Aug 2026 20:34:09 +0530 Subject: [PATCH 02/13] KC-1377: Add --format json support for various Commands (#2265) (#2278) * Add --format json support to whoami and PAM list/info commands * reverted changes for whoami and pam action job-list --- keepercommander/commands/discover/__init__.py | 7 +- .../commands/discover/job_status.py | 275 ++++++++++++------ .../commands/discover/rule_list.py | 55 +++- keepercommander/commands/discoveryrotation.py | 140 ++++++--- .../pam_cloud/pam_privileged_access.py | 101 ++++++- keepercommander/commands/pam_saas/user.py | 155 +++++++--- keepercommander/commands/pam_service/list.py | 135 ++++++--- keepercommander/commands/record.py | 73 ++++- .../commands/tunnel_and_connections.py | 58 +++- 9 files changed, 774 insertions(+), 225 deletions(-) diff --git a/keepercommander/commands/discover/__init__.py b/keepercommander/commands/discover/__init__.py index 6ef3c4b10..5086dfce5 100644 --- a/keepercommander/commands/discover/__init__.py +++ b/keepercommander/commands/discover/__init__.py @@ -270,8 +270,11 @@ def from_gateway(params: KeeperParams, gateway: str, configuration_uid: str | No if application is None: logging.debug(f"cannot find application for gateway {gateway}, skipping.") - if (utils.base64_url_encode(found_gateway.controllerUid) == gateway or - found_gateway.controllerName.lower() == gateway.lower()): + # When --configuration-uid selected the config, trust that selection. + # Otherwise require the gateway name/UID to match. + if (configuration_uid is not None + or utils.base64_url_encode(found_gateway.controllerUid) == gateway + or found_gateway.controllerName.lower() == gateway.lower()): return GatewayContext( configuration=configuration_record, facade=configuration_facade, diff --git a/keepercommander/commands/discover/job_status.py b/keepercommander/commands/discover/job_status.py index 25450489a..fe04492c1 100644 --- a/keepercommander/commands/discover/job_status.py +++ b/keepercommander/commands/discover/job_status.py @@ -1,5 +1,6 @@ from __future__ import annotations import argparse +import json import logging from . import PAMGatewayActionDiscoverCommandBase, GatewayContext from ..pam.router_helper import router_get_connected_gateways @@ -48,6 +49,8 @@ class PAMGatewayActionDiscoverJobStatusCommand(PAMGatewayActionDiscoverCommandBa help='Show history') parser.add_argument('--configuration-uid', '-c', required=False, dest='configuration_uid', action='store', help='PAM configuration UID is using --history') + parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'], + default='table', help='Output format (table, json)') def get_parser(self): return PAMGatewayActionDiscoverJobStatusCommand.parser @@ -155,9 +158,9 @@ def print_job_table(jobs: List[Dict], print("") @staticmethod - def print_job_detail(params: KeeperParams, - all_gateways: List, - job_id: str): + def _build_job_detail(params: KeeperParams, + all_gateways: List, + job_id: str) -> Optional[Dict]: def _find_job(configuration_record) -> Optional[Dict]: jobs_obj = Jobs(record=configuration_record, params=params) @@ -172,96 +175,162 @@ def _find_job(configuration_record) -> Optional[Dict]: find_func=_find_job, gateways=all_gateways) - if gateway_context is not None: - jobs = payload["jobs"] - job = jobs.get_job(job_id) # type: JobItem - infra = Infrastructure(record=gateway_context.configuration, params=params) - - color = bcolors.OKBLUE - status = "RUNNING" - if job.end_ts is not None and not job.error: - if job.success is None: - color = bcolors.WHITE - status = "CANCELLED" + if gateway_context is None: + return None + + jobs = payload["jobs"] + job = jobs.get_job(job_id) + infra = Infrastructure(record=gateway_context.configuration, params=params) + + status = "RUNNING" + if job.end_ts is not None and not job.error: + if job.success is None: + status = "CANCELLED" + else: + status = "COMPLETE" + elif job.error: + status = "FAILED" + + detail = { + "job_id": job.job_id, + "sync_point": job.sync_point, + "gateway": gateway_context.gateway_name, + "gateway_uid": gateway_context.gateway_uid, + "configuration_uid": gateway_context.configuration_uid, + "status": status, + "resource_uid": job.resource_uid or "", + "started": job.start_ts_str or "", + "completed": job.end_ts_str or "", + "duration": job.duration_sec_str or "", + } + + if status == "FAILED": + detail["error"] = job.error + detail["stacktrace"] = job.stacktrace + elif job.end_ts is not None: + try: + infra.load(sync_point=0) + delta_json = job.delta + if delta_json is not None: + delta = DiscoveryDelta.model_validate(delta_json) + added = [] + for item in delta.added: + vertex = infra.dag.get_vertex(item.uid) + if vertex is None or vertex.active is False or vertex.has_data is False: + logging.debug("added: vertex is none, inactive or has no data") + continue + discovery_object = DiscoveryObject.get_discovery_object(vertex) + added.append({ + "uid": item.uid, + "description": discovery_object.description, + }) + + changed = [] + for item in delta.changed: + vertex = infra.dag.get_vertex(item.uid) + if vertex is None or vertex.active is False or vertex.has_data is False: + logging.debug("changed: vertex is none, inactive or has no data") + continue + discovery_object = DiscoveryObject.get_discovery_object(vertex) + changed.append({ + "uid": item.uid, + "description": discovery_object.description, + "changes": item.changes, + }) + + deleted = [{"uid": item.uid} for item in delta.deleted] + detail["delta"] = { + "added": added, + "changed": changed, + "deleted": deleted, + } else: - color = bcolors.OKGREEN - status = "COMPLETE" - elif job.error: - color = bcolors.FAIL - status = "FAILED" + detail["delta"] = None + detail["message"] = "There are no available delta changes for this job." + except Exception as err: + detail["delta_error"] = str(err) + try: + dag = DAG(conn=infra.conn, record=infra.record, + graph_id=PamGraphId.INFRASTRUCTURE) + detail["raw_graph_dot"] = dag.to_dot_raw(sync_point=job.sync_point, rank_dir="RL") + except Exception: + pass - color_status = f"{color}{status}{bcolors.ENDC}" + return detail + @staticmethod + def print_job_detail(params: KeeperParams, + all_gateways: List, + job_id: str): + + detail = PAMGatewayActionDiscoverJobStatusCommand._build_job_detail( + params=params, all_gateways=all_gateways, job_id=job_id) + + if detail is None: + print(f"{bcolors.FAIL}Could not find the gateway with job {job_id}.") + return + + status = detail["status"] + color = bcolors.OKBLUE + if status == "CANCELLED": + color = bcolors.WHITE + elif status == "COMPLETE": + color = bcolors.OKGREEN + elif status == "FAILED": + color = bcolors.FAIL + + color_status = f"{color}{status}{bcolors.ENDC}" + + print("") + print(f"{_h('Job ID')}: {detail['job_id']}") + print(f"{_h('Sync Point')}: {detail['sync_point']}") + print(f"{_h('Gateway Name')}: {detail['gateway']}") + print(f"{_h('Gateway UID')}: {detail['gateway_uid']}") + print(f"{_h('Configuration UID')}: {detail['configuration_uid']}") + print(f"{_h('Status')}: {color_status}") + print(f"{_h('Resource UID')}: {detail['resource_uid'] or 'NA'}") + print(f"{_h('Started')}: {detail['started']}") + print(f"{_h('Completed')}: {detail['completed']}") + print(f"{_h('Duration')}: {detail['duration']}") + + if status == "FAILED": print("") - print(f"{_h('Job ID')}: {job.job_id}") - print(f"{_h('Sync Point')}: {job.sync_point}") - print(f"{_h('Gateway Name')}: {gateway_context.gateway_name}") - print(f"{_h('Gateway UID')}: {gateway_context.gateway_uid}") - print(f"{_h('Configuration UID')}: {gateway_context.configuration_uid}") - print(f"{_h('Status')}: {color_status}") - print(f"{_h('Resource UID')}: {job.resource_uid or 'NA'}") - print(f"{_h('Started')}: {job.start_ts_str}") - print(f"{_h('Completed')}: {job.end_ts_str}") - print(f"{_h('Duration')}: {job.duration_sec_str}") - - # If it failed, show the error and stacktrace. - if status == "FAILED": + print(f"{_h('Gateway Error')}:") + print(f"{color}{detail.get('error')}{bcolors.ENDC}") + print("") + print(f"{_h('Gateway Stacktrace')}:") + print(f"{color}{detail.get('stacktrace')}{bcolors.ENDC}") + elif detail.get("completed"): + if detail.get("delta_error"): + print(f"{_f('Could not load delta from infrastructure: ' + detail['delta_error'])}") + if detail.get("raw_graph_dot"): + print("Fall back to raw graph.") + print("") + print(detail["raw_graph_dot"]) + elif detail.get("delta") is None: print("") - print(f"{_h('Gateway Error')}:") - print(f"{color}{job.error}{bcolors.ENDC}") + print(f"{_f(detail.get('message') or 'There are no available delta changes for this job.')}") + else: + delta = detail["delta"] print("") - print(f"{_h('Gateway Stacktrace')}:") - print(f"{color}{job.stacktrace}{bcolors.ENDC}") - # If it finished, show information about what was discovered. - elif job.end_ts is not None: + print(f"{_h('Added')} - {len(delta['added'])} count") + for item in delta["added"]: + print(f" * {item['description']}") - try: - infra.load(sync_point=0) - print("") - delta_json = job.delta - if delta_json is not None: - delta = DiscoveryDelta.model_validate(delta_json) - print(f"{_h('Added')} - {len(delta.added)} count") - for item in delta.added: - vertex = infra.dag.get_vertex(item.uid) - if vertex is None or vertex.active is False or vertex.has_data is False: - logging.debug("added: vertex is none, inactive or has no data") - continue - discovery_object = DiscoveryObject.get_discovery_object(vertex) - print(f" * {discovery_object.description}") - - print("") - print(f"{_h('Changed')} - {len(delta.changed)} count") - for item in delta.changed: - vertex = infra.dag.get_vertex(item.uid) - if vertex is None or vertex.active is False or vertex.has_data is False: - logging.debug("changed: vertex is none, inactive or has no data") - continue - discovery_object = DiscoveryObject.get_discovery_object(vertex) - print(f" * {discovery_object.description}") - if item.changes is None: - print(" no changed, may be a object not added in prior discoveries.") - else: - for key, value in item.changes.items(): - print(f" - {key} = {value}") - - print("") - print(f"{_h('Deleted')} - {len(delta.deleted)} count") - for item in delta.deleted: - print(f" * discovery vertex {item.uid}") + print("") + print(f"{_h('Changed')} - {len(delta['changed'])} count") + for item in delta["changed"]: + print(f" * {item['description']}") + if item.get("changes") is None: + print(" no changed, may be a object not added in prior discoveries.") else: - print(f"{_f('There are no available delta changes for this job.')}") + for key, value in item["changes"].items(): + print(f" - {key} = {value}") - except Exception as err: - print(f"{_f('Could not load delta from infrastructure: ' + str(err))}") - print("Fall back to raw graph.") - print("") - dag = DAG(conn=infra.conn, record=infra.record, - graph_id=PamGraphId.INFRASTRUCTURE) - print(dag.to_dot_raw(sync_point=job.sync_point, rank_dir="RL")) - - else: - print(f"{bcolors.FAIL}Could not find the gateway with job {job_id}.") + print("") + print(f"{_h('Deleted')} - {len(delta['deleted'])} count") + for item in delta["deleted"]: + print(f" * discovery vertex {item['uid']}") def execute(self, params, **kwargs): @@ -277,6 +346,7 @@ def execute(self, params, **kwargs): # Show the history for the gateway. # gateway_filter needs to be set for show_history = kwargs.get("show_history") + format_type = kwargs.get("format") or "table" # Get all the gateways here so we don't have to keep calling this method. # It gets passed into find_gateway, and find_gateway will pass it around. @@ -292,6 +362,19 @@ def execute(self, params, **kwargs): # If we have a job id, only display information about the one job if job_id: + if format_type == "json": + detail = self._build_job_detail(params=params, + all_gateways=all_gateways, + job_id=job_id) + if detail is None: + print(json.dumps({ + "job": None, + "message": f"Could not find the gateway with job {job_id}." + }, indent=2)) + else: + print(json.dumps({"job": detail}, indent=2)) + return + self.print_job_detail(params=params, all_gateways=all_gateways, job_id=job_id) @@ -301,6 +384,7 @@ def execute(self, params, **kwargs): # Based on parameters set by user, select specific jobs to be displayed. selected_jobs = [] # type: List[Dict] + json_jobs = [] # type: List[Dict] # For each configuration/ gateway, we are going to get all jobs. # We are going to query the gateway for any updated status. @@ -357,10 +441,29 @@ def execute(self, params, **kwargs): job["status"] = "FAILED" selected_jobs.append(job) + json_jobs.append({ + "job_id": job_item.job_id, + "gateway": gateway_context.gateway_name, + "gateway_uid": gateway_context.gateway_uid, + "configuration_uid": gateway_context.configuration_uid, + "status": job["status"], + "resource_uid": getattr(job_item, "resource_uid", None) or "", + "started": job_item.start_ts_str if job_item.start_ts is not None else "", + "completed": job_item.end_ts_str if job_item.end_ts is not None else "", + "duration": job_item.duration_sec_str, + }) if len(selected_jobs) == 0: - print(f"{bcolors.FAIL}There are no discovery jobs. Use 'pam action discover start' to start a " - f"discovery job.{bcolors.ENDC}") + message = ("There are no discovery jobs. Use 'pam action discover start' to start a " + "discovery job.") + if format_type == "json": + print(json.dumps({"jobs": [], "message": message}, indent=2)) + else: + print(f"{bcolors.FAIL}{message}{bcolors.ENDC}") + return + + if format_type == "json": + print(json.dumps({"jobs": json_jobs}, indent=2)) return self.print_job_table(jobs=selected_jobs, diff --git a/keepercommander/commands/discover/rule_list.py b/keepercommander/commands/discover/rule_list.py index 9819fc361..139d8e26d 100644 --- a/keepercommander/commands/discover/rule_list.py +++ b/keepercommander/commands/discover/rule_list.py @@ -1,5 +1,6 @@ from __future__ import annotations import argparse +import json from . import PAMGatewayActionDiscoverCommandBase, GatewayContext, MultiConfigurationException, multi_conf_msg from ...display import bcolors from ..pam.router_helper import router_get_connected_gateways @@ -20,10 +21,29 @@ class PAMGatewayActionDiscoverRuleListCommand(PAMGatewayActionDiscoverCommandBas parser.add_argument('--search', '-s', required=False, dest='search', action='store', help='Search for rules.') + parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'], + default='table', help='Output format (table, json)') def get_parser(self): return PAMGatewayActionDiscoverRuleListCommand.parser + @staticmethod + def _rule_to_dict(rule: RuleItem): + action_value = None + if getattr(rule, 'action', None) is not None: + action_value = rule.action.value + return { + 'rule_id': rule.rule_id, + 'name': rule.name or '', + 'action': action_value, + 'priority': rule.priority, + 'case_sensitive': bool(rule.case_sensitive), + 'added': rule.added_ts_str if rule.added_ts else '', + 'shared_folder_uid': getattr(rule, 'shared_folder_uid', None) or '', + 'admin_uid': getattr(rule, 'admin_uid', None) or '', + 'rule': Rules.make_action_rule_statement_str(rule.statement), + } + @staticmethod def print_rule_table(rule_list: List[RuleItem]): @@ -90,21 +110,43 @@ def execute(self, params, **kwargs): gateway = kwargs.get("gateway") configuration_uid = kwargs.get('configuration_uid') + format_type = kwargs.get('format') or 'table' try: gateway_context = GatewayContext.from_gateway(params=params, gateway=gateway, configuration_uid=configuration_uid) if gateway_context is None: - print(f"{bcolors.FAIL}Could not find the gateway configuration for {gateway}.{bcolors.ENDC}") + message = f'Could not find the gateway configuration for {gateway}.' + if format_type == 'json': + print(json.dumps({'message': message}, indent=2)) + else: + print(f"{bcolors.FAIL}{message}{bcolors.ENDC}") return except MultiConfigurationException as err: - multi_conf_msg(gateway, err) + if format_type == 'json': + configs = [] + for item in (err.items or []): + record = item.get('configuration_record') + if record is not None: + configs.append({ + 'uid': getattr(record, 'record_uid', ''), + 'title': getattr(record, 'title', ''), + }) + print(json.dumps({ + 'message': f'Found multiple configuration records for gateway {gateway}.', + 'configurations': configs, + }, indent=2)) + else: + multi_conf_msg(gateway, err) return rules = Rules(record=gateway_context.configuration, params=params) rule_list = rules.rule_list(rule_type=RuleTypeEnum.ACTION, search=kwargs.get("search")) # type: List[RuleItem] if len(rule_list) == 0: + if format_type == 'json': + print(json.dumps({'rules': []}, indent=2)) + return print("") text = f"{bcolors.FAIL}There are no rules. " \ f"Use 'pam action discover rule add -g {gateway_context.gateway_uid} " @@ -114,4 +156,13 @@ def execute(self, params, **kwargs): print(text) return + if format_type == 'json': + print(json.dumps({ + 'gateway': gateway_context.gateway_name, + 'gateway_uid': gateway_context.gateway_uid, + 'configuration_uid': gateway_context.configuration_uid, + 'rules': [self._rule_to_dict(rule) for rule in rule_list], + }, indent=2)) + return + self.print_rule_table(rule_list=rule_list) diff --git a/keepercommander/commands/discoveryrotation.py b/keepercommander/commands/discoveryrotation.py index 8e8aaa5af..028693d2f 100644 --- a/keepercommander/commands/discoveryrotation.py +++ b/keepercommander/commands/discoveryrotation.py @@ -1683,6 +1683,8 @@ class PAMListRecordRotationCommand(Command): parser = argparse.ArgumentParser(prog='pam rotation list') parser.add_argument('--verbose', '-v', required=False, default=False, dest='is_verbose', action='store_true', help='Verbose output') + parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'], default='table', + help='Output format (table, json)') def get_parser(self): return PAMListRecordRotationCommand.parser @@ -1690,6 +1692,7 @@ def get_parser(self): def execute(self, params, **kwargs): is_verbose = kwargs.get('is_verbose') + format_type = kwargs.get('format') or 'table' rq = pam_pb2.PAMGenericUidsRequest() schedules_proto = router_get_rotation_schedules(params, rq) @@ -1708,19 +1711,27 @@ def execute(self, params, **kwargs): all_pam_config_records = pam_configurations_get_all(params) table = [] - headers = [] - headers.append('Record UID') - headers.append('Record Title') - headers.append('Record Type') - headers.append('Schedule') + if format_type == 'json': + headers = ['record_uid', 'record_title', 'record_type', 'schedule', 'gateway'] + if is_verbose: + headers.append('gateway_uid') + headers.append('pam_configuration') + if is_verbose: + headers.append('pam_configuration_uid') + else: + headers = [] + headers.append('Record UID') + headers.append('Record Title') + headers.append('Record Type') + headers.append('Schedule') - headers.append('Gateway') - if is_verbose: - headers.append('Gateway UID') + headers.append('Gateway') + if is_verbose: + headers.append('Gateway UID') - headers.append('PAM Configuration (Type)') - if is_verbose: - headers.append('PAM Configuration UID') + headers.append('PAM Configuration (Type)') + if is_verbose: + headers.append('PAM Configuration UID') for s in schedules: row = [] @@ -1737,12 +1748,11 @@ def execute(self, params, **kwargs): is_controller_online = any( (poc for poc in enterprise_controllers_connected_uids_bytes if poc == controller_uid)) - row_color = '' if record_exists_in_vault(params, record_uid): - row_color = bcolors.HIGHINTENSITYWHITE + record_accessible = True record_title, record_type = get_vault_record_title_type(params, record_uid) else: - row_color = bcolors.WHITE + record_accessible = False record_title = '[record inaccessible]' record_type = '[record inaccessible]' @@ -1750,9 +1760,15 @@ def execute(self, params, **kwargs): # only pamUser records are supported for rotation continue - row.append(f'{row_color}{record_uid}') - row.append(record_title or '[untitled]') - row.append(record_type or '[unknown]') + if format_type == 'json': + row.append(record_uid) + row.append(record_title or '[untitled]') + row.append(record_type or '[unknown]') + else: + row_color = bcolors.HIGHINTENSITYWHITE if record_accessible else bcolors.WHITE + row.append(f'{row_color}{record_uid}') + row.append(record_title or '[untitled]') + row.append(record_type or '[unknown]') if s.noSchedule is True: # Per Sergey A: @@ -1769,9 +1785,15 @@ def execute(self, params, **kwargs): else: schedule_str = s.scheduleData else: - schedule_str = f'{bcolors.FAIL}[empty]' + schedule_str = '[empty]' - row.append(f'{schedule_str}') + if format_type == 'json': + row.append(schedule_str) + else: + if schedule_str == '[empty]': + row.append(f'{bcolors.FAIL}[empty]') + else: + row.append(f'{schedule_str}') # Controller Info connected_controller = None @@ -1780,29 +1802,45 @@ def execute(self, params, **kwargs): list(enterprise_controllers_connected_resp.controllers)} connected_controller = router_controllers.get(controller_details.controllerUid) - if connected_controller: - controller_stat_color = bcolors.OKGREEN + if format_type == 'json': + if controller_details: + row.append(controller_details.controllerName) + else: + row.append('[Does not exist]') + if is_verbose: + row.append(utils.base64_url_encode(controller_uid)) else: - controller_stat_color = bcolors.WHITE + if connected_controller: + controller_stat_color = bcolors.OKGREEN + else: + controller_stat_color = bcolors.WHITE - controller_color = bcolors.WHITE - if is_controller_online: - controller_color = bcolors.OKGREEN + controller_color = bcolors.WHITE + if is_controller_online: + controller_color = bcolors.OKGREEN - if controller_details: - row.append(f'{controller_stat_color}{controller_details.controllerName}{bcolors.ENDC}') - else: - row.append(f'{controller_stat_color}[Does not exist]{bcolors.ENDC}') + if controller_details: + row.append(f'{controller_stat_color}{controller_details.controllerName}{bcolors.ENDC}') + else: + row.append(f'{controller_stat_color}[Does not exist]{bcolors.ENDC}') - if is_verbose: - row.append(f'{controller_color}{utils.base64_url_encode(controller_uid)}{bcolors.ENDC}') + if is_verbose: + row.append(f'{controller_color}{utils.base64_url_encode(controller_uid)}{bcolors.ENDC}') if not pam_configuration: - if not is_verbose: - row.append(f"{bcolors.FAIL}[No config found]{bcolors.ENDC}") + if format_type == 'json': + if not is_verbose: + row.append('[No config found]') + else: + row.append( + f'[No config found. Looks like configuration {configuration_uid_str} was removed ' + f'but rotation schedule was not modified]') else: - row.append( - f"{bcolors.FAIL}[No config found. Looks like configuration {configuration_uid_str} was removed but rotation schedule was not modified{bcolors.ENDC}") + if not is_verbose: + row.append(f"{bcolors.FAIL}[No config found]{bcolors.ENDC}") + else: + row.append( + f"{bcolors.FAIL}[No config found. Looks like configuration {configuration_uid_str} was removed but rotation schedule was not modified{bcolors.ENDC}") else: pam_config_name, pam_config_type = get_vault_record_title_type(params, configuration_uid_str) @@ -1826,13 +1864,19 @@ def execute(self, params, **kwargs): row.append(f"{pam_config_name or '[untitled]'} ({pam_config_type or '[unknown]'})") if is_verbose: - row.append(f'{utils.base64_url_encode(configuration_uid)}{bcolors.ENDC}') + if format_type == 'json': + row.append(utils.base64_url_encode(configuration_uid)) + else: + row.append(f'{utils.base64_url_encode(configuration_uid)}{bcolors.ENDC}') table.append(row) table.sort(key=lambda x: (x[1] or '')) - dump_report_data(table, headers, fmt='table', filename="", row_number=False, column_width=None) + report = dump_report_data(table, headers, fmt=format_type, filename="", + row_number=False, column_width=None) + if format_type == 'json': + return report print(f"\n{bcolors.OKBLUE}----------------------------------------------------------{bcolors.ENDC}") print(f"{bcolors.OKBLUE}Example to rotate record to which this user has access to:{bcolors.ENDC}") @@ -4217,13 +4261,18 @@ class PAMGatewayActionServerInfoCommand(Command): parser = argparse.ArgumentParser(prog='dr-info-command') parser.add_argument('--gateway', '-g', required=False, dest='gateway_uid', action='store', help='Gateway UID') parser.add_argument('--verbose', '-v', required=False, dest='verbose', action='store_true', help='Verbose Output') + parser.add_argument('--format', dest='format', action='store', choices=['text', 'json'], + default='text', help='Output format (text, json)') def get_parser(self): return PAMGatewayActionServerInfoCommand.parser def execute(self, params, **kwargs): + from .pam.router_helper import get_response_payload + destination_gateway_uid_str = kwargs.get('gateway_uid') is_verbose = kwargs.get('verbose') + format_type = kwargs.get('format') or 'text' router_response = router_send_action_to_gateway( params=params, gateway_action=GatewayActionGatewayInfo(is_scheduled=False), @@ -4232,6 +4281,23 @@ def execute(self, params, **kwargs): destination_gateway_uid_str=destination_gateway_uid_str ) + if format_type == 'json': + if not router_response: + print(json.dumps({"gateway_info": None, "message": "No response from gateway."}, indent=2)) + return + payload = get_response_payload(router_response) + if not (payload.get('is_ok') or payload.get('isOk')): + print(json.dumps({"ok": False, "response": payload}, indent=2)) + return + result = { + "ok": True, + "gateway_info": payload.get('data'), + } + if payload.get('warnings'): + result['warnings'] = payload.get('warnings') + print(json.dumps(result, indent=2)) + return + print_router_response(router_response, 'gateway_info', is_verbose=is_verbose, gateway_uid=destination_gateway_uid_str) diff --git a/keepercommander/commands/pam_cloud/pam_privileged_access.py b/keepercommander/commands/pam_cloud/pam_privileged_access.py index 768302c17..d783057d0 100644 --- a/keepercommander/commands/pam_cloud/pam_privileged_access.py +++ b/keepercommander/commands/pam_cloud/pam_privileged_access.py @@ -417,16 +417,111 @@ class PAMAccessUserListCommand(Command): description='List users in the Identity Provider') parser.add_argument('--config', '-c', required=True, dest='config_uid', help='PAM configuration UID') + parser.add_argument('--format', '-f', dest='output_format', choices=['table', 'json'], + default='table', help='Output format (default: table)') parser.add_argument('--gateway', '-g', dest='gateway', help='Gateway UID or name') def get_parser(self): return PAMAccessUserListCommand.parser + @staticmethod + def _normalize_user(entry): + """Normalize a group member entry into a dict with id/name keys.""" + if isinstance(entry, dict): + user_id = entry.get('id') or entry.get('userId') or entry.get('uid') or '' + name = (entry.get('name') or entry.get('primaryEmail') or entry.get('username') + or entry.get('mail') or entry.get('userPrincipalName') or '') + return {'id': user_id, 'name': name, 'raw': entry} + if isinstance(entry, str): + return {'id': '', 'name': entry, 'raw': {'name': entry}} + return None + def execute(self, params, **kwargs): - raise CommandError('pam-privileged-access', - 'User listing is not yet implemented. ' - 'Use "pam idp group list" to list groups, or check the IdP portal directly.') + config_uid = kwargs['config_uid'] + idp_config_uid = resolve_pam_idp_config(params, config_uid) + + # There is no dedicated IdP user-list gateway action; reuse group list with members. + inputs = GatewayActionIdpInputs( + configuration_uid=config_uid, + idp_config_uid=idp_config_uid, + includeUsers=True, + ) + action = GatewayActionIdpGroupList(inputs=inputs) + + payload = _dispatch_idp_action(params, action, kwargs.get('gateway')) + + response_data = payload.get('data', {}) + if isinstance(response_data, str): + try: + response_data = json.loads(response_data) + except (json.JSONDecodeError, TypeError): + response_data = {} + + if not isinstance(response_data, dict) or not response_data.get('success'): + error = response_data.get('error', 'Unknown error') if isinstance(response_data, dict) else str(response_data) + raise CommandError('pam-privileged-access', f'Gateway reported failure: {error}') + + encrypted_content = response_data.get('data') + if not encrypted_content: + if kwargs.get('output_format') == 'json': + print(json.dumps([], indent=2)) + else: + print('No users found.') + return + + groups = _decrypt_gateway_data(params, config_uid, encrypted_content) + + users_by_key = {} + if isinstance(groups, list): + for group in groups: + if not isinstance(group, dict): + continue + group_info = { + 'id': group.get('id', ''), + 'name': group.get('name', ''), + } + members = group.get('users', []) + if not isinstance(members, list): + continue + for member in members: + normalized = self._normalize_user(member) + if not normalized: + continue + key = (normalized['id'] or normalized['name'] or '').lower() + if not key: + continue + if key not in users_by_key: + users_by_key[key] = { + 'id': normalized['id'], + 'name': normalized['name'], + 'groups': [], + } + users_by_key[key]['groups'].append(group_info) + + users = sorted(users_by_key.values(), key=lambda u: (u.get('name') or '').lower()) + + if kwargs.get('output_format') == 'json': + print(json.dumps(users, indent=2)) + return + + if not users: + print('No users found.') + return + + from keepercommander.commands.base import dump_report_data + headers = ['User ID', 'Name', 'Groups'] + table = [] + for user in users: + group_names = ', '.join( + g.get('name') or g.get('id') or '' for g in user.get('groups', []) + ) + table.append([ + user.get('id', ''), + user.get('name', ''), + group_names, + ]) + dump_report_data(table, headers=headers) # --- Group Commands --- diff --git a/keepercommander/commands/pam_saas/user.py b/keepercommander/commands/pam_saas/user.py index d006c13f0..ecae1f016 100644 --- a/keepercommander/commands/pam_saas/user.py +++ b/keepercommander/commands/pam_saas/user.py @@ -1,5 +1,6 @@ from __future__ import annotations import argparse +import json from ..discover import PAMGatewayActionDiscoverCommandBase, GatewayContext from ...display import bcolors from ... import vault @@ -19,6 +20,8 @@ class PAMActionSaasUserCommand(PAMGatewayActionDiscoverCommandBase): parser.add_argument('--user-record-uid', '-u', required=True, dest='user_uid', action='store', help='The UID of the User record') + parser.add_argument('--format', dest='format', action='store', choices=['text', 'json'], + default='text', help='Output format (text, json)') def get_parser(self): return PAMActionSaasUserCommand.parser @@ -26,35 +29,45 @@ def get_parser(self): def execute(self, params: KeeperParams, **kwargs): user_uid = kwargs.get("user_uid") # type: str + format_type = kwargs.get("format") or "text" + as_json = format_type == "json" - print("") + def _fail(message: str): + if as_json: + print(json.dumps({"user": None, "message": message}, indent=2)) + else: + print("") + print(self._f(message)) + + if not as_json: + print("") # Check to see if the record exists. user_record = vault.KeeperRecord.load(params, user_uid) # type: Optional[TypedRecord] if user_record is None: - print(self._f("The user record does not exists.")) + _fail("The user record does not exists.") return # Make sure this user is a PAM User. if user_record.record_type != PAM_USER: - print(self._f("The user record is not a PAM User.")) + _fail("The user record is not a PAM User.") return record_rotation = params.record_rotation_cache.get(user_record.record_uid) if record_rotation is not None: configuration_uid = record_rotation.get("configuration_uid") else: - print(self._f("The user record does not have any rotation settings.")) + _fail("The user record does not have any rotation settings.") return if configuration_uid is None: - print(self._f("The user record does not have the configuration record set in the rotation settings.")) + _fail("The user record does not have the configuration record set in the rotation settings.") return gateway_context = GatewayContext.from_configuration_uid(params, configuration_uid) if gateway_context is None: - print(self._f("The user record does not have the set gateway")) + _fail("The user record does not have the set gateway") return plugins = get_plugins_map(params, gateway_context) @@ -62,12 +75,19 @@ def execute(self, params: KeeperParams, **kwargs): record_link = RecordLink(record=gateway_context.configuration, params=params, fail_on_corrupt=False) user_vertex = record_link.get_record_link(user_uid) if user_vertex is None: - print(self._f("Cannot find the user in the record link graph.")) + _fail("Cannot find the user in the record link graph.") return - print(self._h(user_record.title)) + result = { + "user": { + "uid": user_record.record_uid, + "title": user_record.title, + }, + "parents": [], + } - missing_configs = [] + if not as_json: + print(self._h(user_record.title)) # User's can have multiple ACL edges to different parents. # One of those ACL edges, in the rotation settings, may a populated saas_record_uid_list @@ -76,27 +96,61 @@ def execute(self, params: KeeperParams, **kwargs): # Check to see if the record exists. parent_record = vault.KeeperRecord.load(params, parent_vertex.uid) # type: Optional[TypedRecord] if parent_record is None: - print(self._f(f"* Parent record UID {parent_vertex.uid} does not exists.")) - print(" The record may have been deleted, however the relationship still exists.") - print("") + parent_entry = { + "uid": parent_vertex.uid, + "title": None, + "record_type": None, + "message": "Parent record does not exist. The record may have been deleted, " + "however the relationship still exists.", + "saas_configs": [], + } + result["parents"].append(parent_entry) + if not as_json: + print(self._f(f"* Parent record UID {parent_vertex.uid} does not exists.")) + print(" The record may have been deleted, however the relationship still exists.") + print("") continue - print(self._b(f" * {parent_record.title}, {parent_record.record_type}")) - print("") + parent_entry = { + "uid": parent_record.record_uid, + "title": parent_record.title, + "record_type": parent_record.record_type, + "saas_configs": [], + } + + if not as_json: + print(self._b(f" * {parent_record.title}, {parent_record.record_type}")) + print("") acl = record_link.get_acl(user_uid, parent_vertex.uid) if acl is not None and acl.rotation_settings is not None: saas_record_uid_list = acl.rotation_settings.saas_record_uid_list if saas_record_uid_list is None or len(saas_record_uid_list) == 0: - print(f"{bcolors.WARNING} The user does not have any SaaS service rotations.{bcolors.ENDC}") + message = "The user does not have any SaaS service rotations." + parent_entry["message"] = message + result["parents"].append(parent_entry) + if as_json: + print(json.dumps(result, indent=2)) + else: + print(f"{bcolors.WARNING} {message}{bcolors.ENDC}") return for config_record_uid in saas_record_uid_list: config_record = vault.KeeperRecord.load(params, config_record_uid) # type: Optional[TypedRecord] if config_record is None: - print(f"{bcolors.WARNING} * Record UID {config_record_uid} not longer exists.{bcolors.ENDC}") + missing = { + "uid": config_record_uid, + "title": None, + "message": "Record no longer exists.", + } + parent_entry["saas_configs"].append(missing) + if not as_json: + print(f"{bcolors.WARNING} * Record UID {config_record_uid} not longer exists." + f"{bcolors.ENDC}") continue - print(self._gr(f" {config_record.title}")) + + if not as_json: + print(self._gr(f" {config_record.title}")) plugin_name = "" saas_type_field = next((x for x in config_record.custom if x.label == "SaaS Type"), None) @@ -105,27 +159,20 @@ def execute(self, params: KeeperParams, **kwargs): plugin_name = saas_type_field.value[0] plugin = plugins.get(plugin_name) + supported = plugin is not None - # This might have been a valid plugin, or the name is mistyped, so it's not supported. - if plugin is None: - plugin_name += " (" + self._f("Not Supported") + ")" - - rotation_active = self._gr("Active") + is_active = True rotation_active_field = next((x for x in config_record.custom if x.label == "Active"), None) if (rotation_active_field is not None and rotation_active_field.value is not None and len(rotation_active_field.value) > 0): is_active = value_to_boolean(rotation_active_field.value[0]) - if is_active is False: - rotation_active = self._f("Inactive") - - print(f" {bcolors.BOLD}SaaS Type{bcolors.ENDC}: {plugin_name}") - print(f" {bcolors.BOLD}Config Record UID{bcolors.ENDC}: {config_record.record_uid}") - print(f" {bcolors.BOLD}Active{bcolors.ENDC}: {rotation_active}") + if is_active is None: + is_active = True + fields = {} if plugin is not None: - for field in plugin.fields: value = next((x.value for x in config_record.custom if x.label == field.label), None) if value is not None: @@ -133,10 +180,48 @@ def execute(self, params: KeeperParams, **kwargs): value = value[0] else: value = None - if value is None: - if field.default_value is not None: - value = f"{field.default_value} ({bcolors.OKBLUE}Default{bcolors.ENDC})" - else: + field_info = { + "value": value, + "default": False, + "set": value is not None, + } + if value is None and field.default_value is not None: + field_info["value"] = field.default_value + field_info["default"] = True + field_info["set"] = False + fields[field.label] = field_info + + saas_entry = { + "uid": config_record.record_uid, + "title": config_record.title, + "saas_type": plugin_name, + "supported": supported, + "active": bool(is_active), + "fields": fields, + } + parent_entry["saas_configs"].append(saas_entry) + + if not as_json: + display_plugin = plugin_name + if not supported: + display_plugin += " (" + self._f("Not Supported") + ")" + rotation_active = self._gr("Active") if is_active else self._f("Inactive") + print(f" {bcolors.BOLD}SaaS Type{bcolors.ENDC}: {display_plugin}") + print(f" {bcolors.BOLD}Config Record UID{bcolors.ENDC}: {config_record.record_uid}") + print(f" {bcolors.BOLD}Active{bcolors.ENDC}: {rotation_active}") + + if plugin is not None: + for field in plugin.fields: + field_info = fields[field.label] + value = field_info["value"] + if not field_info["set"] and field_info["default"]: + value = f"{value} ({bcolors.OKBLUE}Default{bcolors.ENDC})" + elif not field_info["set"]: value = f"{bcolors.FAIL}Not Set{bcolors.ENDC}" - print(f" {bcolors.BOLD}{field.label}{bcolors.ENDC}: {value}") - print("") + print(f" {bcolors.BOLD}{field.label}{bcolors.ENDC}: {value}") + print("") + + result["parents"].append(parent_entry) + + if as_json: + print(json.dumps(result, indent=2)) diff --git a/keepercommander/commands/pam_service/list.py b/keepercommander/commands/pam_service/list.py index df571a3d9..bdb5e99c2 100644 --- a/keepercommander/commands/pam_service/list.py +++ b/keepercommander/commands/pam_service/list.py @@ -1,5 +1,6 @@ from __future__ import annotations import argparse +import json from ..discover import PAMGatewayActionDiscoverCommandBase, GatewayContext, MultiConfigurationException, multi_conf_msg from ...display import bcolors from ... import vault @@ -26,11 +27,13 @@ class PAMActionServiceListCommand(PAMGatewayActionDiscoverCommandBase): action='store', help='PAM configuration UID, if gateway has multiple.') parser.add_argument('--by-machine', '-m', required=False, dest='do_by_machine', action='store_true', help='List by machine') + parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'], + default='table', help='Output format (table, json)') def get_parser(self): return PAMActionServiceListCommand.parser - def _by_user(self, params: KeeperParams, record_link: RecordLink, user_service: UserService): + def _collect_by_user(self, params: KeeperParams, record_link: RecordLink, user_service: UserService): service_map = {} for resource_vertex in record_link.dag.get_root.has_vertices(edge_type=EdgeType.LINK): @@ -63,31 +66,18 @@ def _by_user(self, params: KeeperParams, record_link: RecordLink, user_service: if user_record.record_uid not in service_map: service_map[user_record.record_uid] = { "title": user_record.title, + "uid": user_record.record_uid, "active": user_active, "machines": [] } - text = f"{resource_record.title} ({resource_record.record_uid})" - if not resource_active: - text += f" : {bcolors.FAIL}Disabled{bcolors.ENDC}" - service_map[user_record.record_uid]["machines"].append(text) - - print("") - printed_something = False - print(self._h("User Mapping")) - for user_uid in service_map: - user = service_map[user_uid] - printed_something = True - active_text = "" - if not user['active']: - active_text = f" {bcolors.FAIL}Disabled{bcolors.ENDC}" - print(f" {self._b(user['title'])} ({user_uid}){active_text}") - for machine in user["machines"]: - print(f" * {machine}") - print("") - if not printed_something: - print(f" {bcolors.FAIL}There are no service mappings.{bcolors.ENDC}") - - def _by_machine(self, params: KeeperParams, record_link: RecordLink, user_service: UserService): + service_map[user_record.record_uid]["machines"].append({ + "title": resource_record.title, + "uid": resource_record.record_uid, + "active": resource_active, + }) + return service_map + + def _collect_by_machine(self, params: KeeperParams, record_link: RecordLink, user_service: UserService): service_map = {} for resource_vertex in record_link.dag.get_root.has_vertices(edge_type=EdgeType.LINK): resource_record = vault.KeeperRecord.load(params, resource_vertex.uid) # type: Optional[TypedRecord] @@ -119,26 +109,53 @@ def _by_machine(self, params: KeeperParams, record_link: RecordLink, user_servic if resource_record.record_uid not in service_map: service_map[resource_record.record_uid] = { "title": resource_record.title, + "uid": resource_record.record_uid, "active": resource_active, "users": [] } - text = f"{user_record.title} ({user_record.record_uid})" - if not user_active: - text += f" : {bcolors.FAIL}Disabled{bcolors.ENDC}" - service_map[resource_record.record_uid]["users"].append(text) + service_map[resource_record.record_uid]["users"].append({ + "title": user_record.title, + "uid": user_record.record_uid, + "active": user_active, + }) + return service_map + + def _print_by_user(self, service_map): + print("") + printed_something = False + print(self._h("User Mapping")) + for user_uid in service_map: + user = service_map[user_uid] + printed_something = True + active_text = "" + if not user['active']: + active_text = f" {bcolors.FAIL}Disabled{bcolors.ENDC}" + print(f" {self._b(user['title'])} ({user_uid}){active_text}") + for machine in user["machines"]: + text = f"{machine['title']} ({machine['uid']})" + if not machine['active']: + text += f" : {bcolors.FAIL}Disabled{bcolors.ENDC}" + print(f" * {text}") + print("") + if not printed_something: + print(f" {bcolors.FAIL}There are no service mappings.{bcolors.ENDC}") + def _print_by_machine(self, service_map): print("") printed_something = False print(self._h("Machine Mapping")) for resource_uid in service_map: - user = service_map[resource_uid] + resource = service_map[resource_uid] printed_something = True active_text = "" - if not user['active']: + if not resource['active']: active_text = f" {bcolors.FAIL}Disabled{bcolors.ENDC}" - print(f" {self._b(user['title'])} ({resource_uid}){active_text}") - for user in user["users"]: - print(f" * {user}") + print(f" {self._b(resource['title'])} ({resource_uid}){active_text}") + for user in resource["users"]: + text = f"{user['title']} ({user['uid']})" + if not user['active']: + text += f" : {bcolors.FAIL}Disabled{bcolors.ENDC}" + print(f" * {text}") print("") if not printed_something: print(f" {bcolors.FAIL}There are no service mappings.{bcolors.ENDC}") @@ -146,16 +163,35 @@ def _by_machine(self, params: KeeperParams, record_link: RecordLink, user_servic def execute(self, params: KeeperParams, **kwargs): gateway = kwargs.get("gateway", "none_set") + format_type = kwargs.get('format') or 'table' try: gateway_context = GatewayContext.from_gateway(params=params, gateway=gateway, configuration_uid=kwargs.get('configuration_uid')) if gateway_context is None: - print(f"{bcolors.FAIL}Could not find the gateway configuration for {gateway}.{bcolors.ENDC}") + message = f'Could not find the gateway configuration for {gateway}.' + if format_type == 'json': + print(json.dumps({'message': message}, indent=2)) + else: + print(f"{bcolors.FAIL}{message}{bcolors.ENDC}") return except MultiConfigurationException as err: - multi_conf_msg(gateway, err) + if format_type == 'json': + configs = [] + for item in (err.items or []): + record = item.get('configuration_record') + if record is not None: + configs.append({ + 'uid': getattr(record, 'record_uid', ''), + 'title': getattr(record, 'title', ''), + }) + print(json.dumps({ + 'message': f'Found multiple configuration records for gateway {gateway}.', + 'configurations': configs, + }, indent=2)) + else: + multi_conf_msg(gateway, err) return record_link = RecordLink(record=gateway_context.configuration, @@ -171,11 +207,28 @@ def execute(self, params: KeeperParams, **kwargs): fail_on_corrupt=False, agent=f"Cmdr/{__version__}") - if kwargs.get("do_by_machine"): - self._by_machine(params=params, - record_link=record_link, - user_service=user_service) + by_machine = bool(kwargs.get("do_by_machine")) + if by_machine: + service_map = self._collect_by_machine(params=params, + record_link=record_link, + user_service=user_service) + else: + service_map = self._collect_by_user(params=params, + record_link=record_link, + user_service=user_service) + + if format_type == 'json': + payload = { + 'gateway': gateway_context.gateway_name, + 'gateway_uid': gateway_context.gateway_uid, + 'configuration_uid': gateway_context.configuration_uid, + 'group_by': 'machine' if by_machine else 'user', + 'mappings': list(service_map.values()), + } + print(json.dumps(payload, indent=2)) + return + + if by_machine: + self._print_by_machine(service_map) else: - self._by_user(params=params, - record_link=record_link, - user_service=user_service) + self._print_by_user(service_map) diff --git a/keepercommander/commands/record.py b/keepercommander/commands/record.py index c8c87fc93..2fa86f0df 100644 --- a/keepercommander/commands/record.py +++ b/keepercommander/commands/record.py @@ -1990,6 +1990,8 @@ def fetch_members(team_uid): # type: (str) -> List[str] trash_get_parser = argparse.ArgumentParser(prog='trash get', description='Get the details of a deleted record') +trash_get_parser.add_argument('--format', dest='format', action='store', choices=['detail', 'json'], + default='detail', help='output format') trash_get_parser.add_argument('record', action='store', help='Deleted record UID') trash_restore_parser = argparse.ArgumentParser(prog='trash restore', description='Restores deleted records') @@ -2308,13 +2310,20 @@ def get_parser(self): def execute(self, params, **kwargs): deleted_records = self.get_deleted_records(params) orphaned_records = self.get_orphaned_records(params) + fmt = kwargs.get('format') or 'detail' if len(deleted_records) == 0 and len(orphaned_records) == 0: - logging.info('Trash is empty') + if fmt == 'json': + print(json.dumps({'message': 'Trash is empty'}, indent=2)) + else: + logging.info('Trash is empty') return record_uid = kwargs.get('record') if not record_uid: - logging.info('Record UID parameter is required') + if fmt == 'json': + print(json.dumps({'message': 'Record UID parameter is required'}, indent=2)) + else: + logging.info('Record UID parameter is required') return is_shared = False @@ -2323,12 +2332,68 @@ def execute(self, params, **kwargs): rec = orphaned_records.get(record_uid) is_shared = True if not rec: - logging.info('%s is not a valid deleted record UID', record_uid) + message = f'{record_uid} is not a valid deleted record UID' + if fmt == 'json': + print(json.dumps({'message': message}, indent=2)) + else: + logging.info('%s is not a valid deleted record UID', record_uid) return record = vault.KeeperRecord.load(params, rec) if not record: - logging.info('Cannot restore record %s', record_uid) + message = f'Cannot restore record {record_uid}' + if fmt == 'json': + print(json.dumps({'message': message}, indent=2)) + else: + logging.info('Cannot restore record %s', record_uid) + return + + if fmt == 'json': + payload = { + 'record_uid': record.record_uid, + 'title': record.title, + 'record_type': record.record_type, + 'status': 'Share' if is_shared else 'Record', + 'fields': {}, + } + for name, value in record.enumerate_fields(): + if value: + if isinstance(value, list): + payload['fields'][name] = value + elif len(value) > 100: + payload['fields'][name] = value[:99] + '...' + else: + payload['fields'][name] = value + + if is_shared: + if 'shares' not in rec: + rec['shares'] = {} + shares = api.get_record_shares(params, (record_uid,), True) + if isinstance(shares, list): + record_shares = next( + (x.get('shares') for x in shares if x.get('record_uid') == record_uid), None) + if isinstance(record_shares, dict): + rec['shares'] = record_shares + + user_shares = [] + if 'shares' in rec and 'user_permissions' in rec['shares']: + for uo in rec['shares']['user_permissions']: + if uo.get('owner'): + continue + flags = [] + if uo.get('editable'): + flags.append('Can Edit') + if uo.get('shareable'): + flags.append('Can Share') + user_shares.append({ + 'username': uo.get('username'), + 'permissions': ' & '.join(flags) if flags else 'Read Only', + 'self': uo.get('username') == params.user, + }) + if user_shares: + payload['direct_user_shares'] = user_shares + + print(json.dumps(payload, indent=2, default=base.json_serialized)) return for name, value in record.enumerate_fields(): diff --git a/keepercommander/commands/tunnel_and_connections.py b/keepercommander/commands/tunnel_and_connections.py index 2b478e83e..32cb55b9a 100644 --- a/keepercommander/commands/tunnel_and_connections.py +++ b/keepercommander/commands/tunnel_and_connections.py @@ -124,13 +124,19 @@ def __init__(self): # Individual Commands class PAMTunnelListCommand(Command): pam_cmd_parser = argparse.ArgumentParser(prog='pam tunnel list') + pam_cmd_parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'], + default='table', help='Output format (table, json)') def get_parser(self): return PAMTunnelListCommand.pam_cmd_parser def execute(self, params, **kwargs): + format_type = kwargs.get('format') or 'table' table = [] - headers = ['Record', 'Remote Target', 'Local Address', 'Tunnel ID', 'Conversation ID', 'Status'] + if format_type == 'json': + headers = ['record', 'remote_target', 'local_address', 'tunnel_id', 'conversation_id', 'status'] + else: + headers = ['Record', 'Remote Target', 'Local Address', 'Tunnel ID', 'Conversation ID', 'Status'] # In-process tunnels from the Rust PyTubeRegistry tube_registry = get_or_create_tube_registry(params) @@ -142,28 +148,39 @@ def execute(self, params, **kwargs): conversation_ids = tube_registry.get_conversation_ids_by_tube_id(tube_id) tunnel_session = get_tunnel_session(tube_id) - record_title = tunnel_session.record_title if tunnel_session and tunnel_session.record_title else f"{bcolors.WARNING}unknown{bcolors.ENDC}" + if tunnel_session and tunnel_session.record_title: + record_title = tunnel_session.record_title + else: + record_title = 'unknown' if tunnel_session and tunnel_session.target_host and tunnel_session.target_port: remote_target = f"{tunnel_session.target_host}:{tunnel_session.target_port}" else: - remote_target = f"{bcolors.WARNING}unknown{bcolors.ENDC}" + remote_target = 'unknown' if tunnel_session and tunnel_session.host and tunnel_session.port: - local_addr = f"{bcolors.OKGREEN}{tunnel_session.host}:{tunnel_session.port}{bcolors.ENDC}" + local_addr = f"{tunnel_session.host}:{tunnel_session.port}" else: - local_addr = f"{bcolors.WARNING}unknown{bcolors.ENDC}" + local_addr = 'unknown' conv_id = conversation_ids[0] if conversation_ids else (tunnel_session.conversation_id if tunnel_session else 'none') try: state = tube_registry.get_connection_state(tube_id) - status_color = f"{bcolors.OKGREEN}" if state.lower() == "connected" else f"{bcolors.WARNING}" - status = f"{status_color}{state}{bcolors.ENDC}" + status = state except Exception: - status = f"{bcolors.WARNING}unknown{bcolors.ENDC}" + status = 'unknown' - table.append([record_title, remote_target, local_addr, tube_id, conv_id, status]) + if format_type == 'json': + table.append([record_title, remote_target, local_addr, tube_id, conv_id, status]) + else: + rec_disp = record_title if record_title != 'unknown' else f"{bcolors.WARNING}unknown{bcolors.ENDC}" + rem_disp = remote_target if remote_target != 'unknown' else f"{bcolors.WARNING}unknown{bcolors.ENDC}" + loc_disp = (f"{bcolors.OKGREEN}{local_addr}{bcolors.ENDC}" + if local_addr != 'unknown' else f"{bcolors.WARNING}unknown{bcolors.ENDC}") + status_color = f"{bcolors.OKGREEN}" if status.lower() == "connected" else f"{bcolors.WARNING}" + status_disp = f"{status_color}{status}{bcolors.ENDC}" + table.append([rec_disp, rem_disp, loc_disp, tube_id, conv_id, status_disp]) # Cross-process tunnels from the file-based registry for entry in list_registered_tunnels(): @@ -173,20 +190,31 @@ def execute(self, params, **kwargs): rec = entry.get('record_title') or entry.get('record_uid', '?') th = entry.get('target_host') tp = entry.get('target_port') - remote = f"{th}:{tp}" if th and tp else f"{bcolors.WARNING}n/a{bcolors.ENDC}" + remote = f"{th}:{tp}" if th and tp else 'n/a' h = entry.get('host', '127.0.0.1') p = entry.get('port', '?') - local = f"{bcolors.OKGREEN}{h}:{p}{bcolors.ENDC}" + local = f"{h}:{p}" tid = entry.get('tube_id', '') mode = entry.get('mode', '?') - status = f"{bcolors.OKGREEN}{mode} (PID {pid}){bcolors.ENDC}" - table.append([rec, remote, local, tid, '', status]) + status = f"{mode} (PID {pid})" + if format_type == 'json': + table.append([rec, remote, local, tid, '', status]) + else: + rem_disp = remote if remote != 'n/a' else f"{bcolors.WARNING}n/a{bcolors.ENDC}" + table.append([rec, rem_disp, f"{bcolors.OKGREEN}{local}{bcolors.ENDC}", tid, '', + f"{bcolors.OKGREEN}{status}{bcolors.ENDC}"]) if not table: - logging.warning(f"{bcolors.OKBLUE}No Tunnels running{bcolors.ENDC}") + message = "No Tunnels running" + if format_type == 'json': + print(json.dumps({"tunnels": [], "message": message}, indent=2)) + else: + logging.warning(f"{bcolors.OKBLUE}{message}{bcolors.ENDC}") return - dump_report_data(table, headers, fmt='table', filename="", row_number=False, column_width=None) + report = dump_report_data(table, headers, fmt=format_type, filename="", row_number=False, column_width=None) + if format_type == 'json': + return report class PAMTunnelStopCommand(Command): From abd0d514ebb0b6272033c8e6151e2282b677343a Mon Sep 17 00:00:00 2001 From: amangalampalli-ks Date: Mon, 10 Aug 2026 20:34:51 +0530 Subject: [PATCH 03/13] KC-1392: Block PAM workflow admin commands after workflow policy is revoked. (#2271) (#2279) * Block PAM workflow admin commands after workflow-settings policy is revoked. * Fix review comments. --- .../commands/pam_import/workflow_apply.py | 3 + .../commands/workflow/config_commands.py | 20 +++- keepercommander/commands/workflow/helpers.py | 64 +++++++++++ keepercommander/commands/workflow/registry.py | 18 ++- .../pam/test_workflow_enforcement_gate.py | 108 ++++++++++++++++++ 5 files changed, 201 insertions(+), 12 deletions(-) create mode 100644 unit-tests/pam/test_workflow_enforcement_gate.py diff --git a/keepercommander/commands/pam_import/workflow_apply.py b/keepercommander/commands/pam_import/workflow_apply.py index 65ffc8c5f..c02236f33 100644 --- a/keepercommander/commands/pam_import/workflow_apply.py +++ b/keepercommander/commands/pam_import/workflow_apply.py @@ -19,6 +19,7 @@ RecordResolver, WorkflowFormatter, sanitize_router_error, + ensure_can_configure_workflow_settings, ) from ...error import CommandError, KeeperApiError from ...params import KeeperParams @@ -192,6 +193,8 @@ def apply_workflow( opts: PamWorkflowOptions, ) -> None: """Create or update workflow config via Krouter. Raises CommandError on failure.""" + ensure_can_configure_workflow_settings(params, refresh=True, action='apply') + record_uid_bytes = utils.base64_url_decode(record_uid) ref = ProtobufRefBuilder.record_ref(record_uid_bytes, record_title) diff --git a/keepercommander/commands/workflow/config_commands.py b/keepercommander/commands/workflow/config_commands.py index 1fe17d59a..670a8da4e 100644 --- a/keepercommander/commands/workflow/config_commands.py +++ b/keepercommander/commands/workflow/config_commands.py @@ -22,7 +22,13 @@ from ...proto import workflow_pb2 from ... import utils -from .helpers import RecordResolver, ProtobufRefBuilder, WorkflowFormatter, sanitize_router_error +from .helpers import ( + RecordResolver, + ProtobufRefBuilder, + WorkflowFormatter, + sanitize_router_error, + ensure_can_configure_workflow_settings, +) def _add_approvers_to_workflow(params, record_uid, record_name, @@ -89,6 +95,10 @@ def get_parser(self): return WorkflowCreateCommand.parser def execute(self, params: KeeperParams, **kwargs): + # Defense in depth: registry also gates admin verbs after refreshing + # enforcements. Re-check here so direct execute() cannot skip the policy. + ensure_can_configure_workflow_settings(params, refresh=True, action='create') + record_uid, record = RecordResolver.resolve(params, kwargs.get('record')) RecordResolver.validate_workflow_record_type(record) record_uid_bytes = utils.base64_url_decode(record_uid) @@ -376,6 +386,8 @@ def get_parser(self): return WorkflowUpdateCommand.parser def execute(self, params: KeeperParams, **kwargs): + ensure_can_configure_workflow_settings(params, refresh=True, action='update') + record_uid, record = RecordResolver.resolve(params, kwargs.get('record')) record_uid_bytes = utils.base64_url_decode(record_uid) @@ -456,6 +468,8 @@ def get_parser(self): return WorkflowDeleteCommand.parser def execute(self, params: KeeperParams, **kwargs): + ensure_can_configure_workflow_settings(params, refresh=True, action='delete') + record_uid, record = RecordResolver.resolve(params, kwargs.get('record')) record_uid_bytes = utils.base64_url_decode(record_uid) ref = ProtobufRefBuilder.record_ref(record_uid_bytes, record.title) @@ -511,6 +525,8 @@ def get_parser(self): return WorkflowAddApproversCommand.parser def execute(self, params: KeeperParams, **kwargs): + ensure_can_configure_workflow_settings(params, refresh=True, action='add-approver') + users = list(dict.fromkeys( u.strip() for u in (kwargs.get('user') or []) if u and u.strip() )) @@ -582,6 +598,8 @@ def get_parser(self): return WorkflowDeleteApproversCommand.parser def execute(self, params: KeeperParams, **kwargs): + ensure_can_configure_workflow_settings(params, refresh=True, action='remove-approver') + users = kwargs.get('user') or [] teams = kwargs.get('team') or [] diff --git a/keepercommander/commands/workflow/helpers.py b/keepercommander/commands/workflow/helpers.py index 96e49c4fd..d0dc1999d 100644 --- a/keepercommander/commands/workflow/helpers.py +++ b/keepercommander/commands/workflow/helpers.py @@ -9,8 +9,10 @@ # Contact: ops@keepersecurity.com # +import logging import re import shlex +import time from typing import List, Optional, Tuple from ...error import CommandError @@ -24,6 +26,68 @@ ) _RESPONSE_CODE_RE = re.compile(r'\s*[Rr]esponse\s+code:\s*\S+\s*$') +# Enterprise role enforcement: "Can manage workflow settings" +WORKFLOW_SETTINGS_ENFORCEMENT_KEY = 'allow_configure_workflow_settings' + +# Avoid duplicate account-summary calls when registry + command both refresh. +_ENFORCEMENT_REFRESH_TTL_SEC = 2.0 +_ENFORCEMENT_REFRESH_ATTR = '_workflow_enforcement_refreshed_at' + + +def refresh_enforcements(params: KeeperParams) -> None: + """Reload account summary so revoked role enforcements take effect without re-login.""" + now = time.monotonic() + last = getattr(params, _ENFORCEMENT_REFRESH_ATTR, 0.0) + if not isinstance(last, (int, float)): + last = 0.0 + if now - last < _ENFORCEMENT_REFRESH_TTL_SEC: + return + try: + from ...loginv3 import LoginV3Flow + LoginV3Flow.populateAccountSummary(params) + setattr(params, _ENFORCEMENT_REFRESH_ATTR, now) + except Exception as e: + logging.error('Failed to refresh enforcements: %s', e, exc_info=True) + + +def can_configure_workflow_settings(params: KeeperParams, *, refresh: bool = False) -> bool: + """True when the user currently has allow_configure_workflow_settings. + + When refresh=True, reloads account summary first so a mid-session policy revoke + is visible (plain sync-down does not refresh enforcements). + """ + if refresh: + refresh_enforcements(params) + enforcements = getattr(params, 'enforcements', None) + if not enforcements or 'booleans' not in enforcements: + return False + booleans = enforcements.get('booleans') or [] + if not isinstance(booleans, list): + return False + return any( + bool(b.get('value')) + for b in booleans + if isinstance(b, dict) and b.get('key') == WORKFLOW_SETTINGS_ENFORCEMENT_KEY + ) + + +def ensure_can_configure_workflow_settings( + params: KeeperParams, + *, + refresh: bool = True, + action: str = 'manage', +) -> None: + """Raise CommandError unless the user may configure workflow settings.""" + if can_configure_workflow_settings(params, refresh=refresh): + return + raise CommandError( + '', + f'You do not have permission to manage workflow settings. ' + f'The "{action}" command requires the "Can manage workflow settings" ' + f'enforcement policy. Contact your Keeper administrator to enable this ' + f'for your role.', + ) + class DashUidArgsMixin: """Mixin for commands whose positional flow-UID arg may start with '-' (base64url).""" diff --git a/keepercommander/commands/workflow/registry.py b/keepercommander/commands/workflow/registry.py index 5353dfa52..8e8a4b66d 100644 --- a/keepercommander/commands/workflow/registry.py +++ b/keepercommander/commands/workflow/registry.py @@ -12,8 +12,7 @@ from ..base import GroupCommand, dump_report_data from ...display import bcolors -_ENFORCEMENT_KEY = 'allow_configure_workflow_settings' - +from .helpers import can_configure_workflow_settings from .config_commands import ( WorkflowCreateCommand, WorkflowReadCommand, @@ -43,14 +42,8 @@ class PAMWorkflowCommand(GroupCommand): _ADMIN_VERBS = frozenset({'create', 'update', 'delete', 'add-approver', 'remove-approver'}) @staticmethod - def _can_manage_workflows(params): - enforcements = getattr(params, 'enforcements', None) - if not enforcements or 'booleans' not in enforcements: - return False - return any( - b.get('value') for b in enforcements['booleans'] - if b.get('key') == _ENFORCEMENT_KEY - ) + def _can_manage_workflows(params, *, refresh=False): + return can_configure_workflow_settings(params, refresh=refresh) def execute_args(self, params, args, **kwargs): self._current_params = params @@ -59,7 +52,10 @@ def execute_args(self, params, args, **kwargs): verb = (args[:pos].strip() if pos > 0 else args.strip()).lower() if args else '' resolved_verb = self._aliases.get(verb, verb) - if resolved_verb in self._ADMIN_VERBS and not self._can_manage_workflows(params): + # Refresh enforcements before admin verbs so a mid-session revoke of + # "Can manage workflow settings" blocks create/update/delete equally. + # (Plain sync-down does not reload account enforcements.) + if resolved_verb in self._ADMIN_VERBS and not self._can_manage_workflows(params, refresh=True): print( f"\n{bcolors.WARNING}You do not have permission to manage workflow settings.{bcolors.ENDC}\n" f"The '{bcolors.BOLD}{resolved_verb}{bcolors.ENDC}' command requires the " diff --git a/unit-tests/pam/test_workflow_enforcement_gate.py b/unit-tests/pam/test_workflow_enforcement_gate.py new file mode 100644 index 000000000..b8f16e273 --- /dev/null +++ b/unit-tests/pam/test_workflow_enforcement_gate.py @@ -0,0 +1,108 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' Date: Mon, 10 Aug 2026 11:23:54 -0700 Subject: [PATCH 04/13] Delete unused files --- keeper-win-file.spec | 51 - keeper_file.spec | 43 - keeper_folder.spec | 47 - public_suffix_list.dat | 13756 --------------------------------------- 4 files changed, 13897 deletions(-) delete mode 100644 keeper-win-file.spec delete mode 100644 keeper_file.spec delete mode 100644 keeper_folder.spec delete mode 100644 public_suffix_list.dat diff --git a/keeper-win-file.spec b/keeper-win-file.spec deleted file mode 100644 index 4f3cb975b..000000000 --- a/keeper-win-file.spec +++ /dev/null @@ -1,51 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - - -a = Analysis(['keeper.py'], - pathex=['.'], - binaries=[], - datas=[(os.path.join('keepercommander', 'importer', 'keepass', 'template.kdbx'), - os.path.join('keepercommander', 'importer', 'keepass')), - ('public_suffix_list.dat', 'fido2'), - ], - hiddenimports=[ - 'fido2', - 'ldap3', - 'awscli', - 'msal', - 'json', - 'csv', - 'lxml', - 'pykeepass', - 'msvcrt', - 'keepercommander.importer.json', - 'keepercommander.importer.csv', - 'keepercommander.importer.keepass', - 'keepercommander.importer.lastpass', - 'keepercommander.plugins', - 'keepercommander.yubikey'], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='keeper', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True ) diff --git a/keeper_file.spec b/keeper_file.spec deleted file mode 100644 index dafee497d..000000000 --- a/keeper_file.spec +++ /dev/null @@ -1,43 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - -import os - -a = Analysis(['keeper.py'], - pathex=['.'], - binaries=[], - datas=[(os.path.join('keepercommander', 'importer', 'keepass', 'template.kdbx'), - os.path.join('keepercommander', 'importer', 'keepass')), - ('public_suffix_list.dat', 'fido2'), - ], - hiddenimports=['keepercommander.importer.json', - 'keepercommander.importer.csv', - 'keepercommander.importer.keepass', - 'keepercommander.importer.lastpass', - 'keepercommander.plugins', - 'keepercommander.yubikey', - ], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='keeper', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=False, - upx_exclude=[], - runtime_tmpdir=None, - console=True ) diff --git a/keeper_folder.spec b/keeper_folder.spec deleted file mode 100644 index fe770231e..000000000 --- a/keeper_folder.spec +++ /dev/null @@ -1,47 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - -import os - -a = Analysis(['keeper.py'], - pathex=['.'], - binaries=[], - datas=[(os.path.join('keepercommander', 'importer', 'keepass', 'template.kdbx'), - os.path.join('keepercommander', 'importer', 'keepass')), - ('public_suffix_list.dat', 'fido2'), - ], - hiddenimports=['keepercommander.importer.json', - 'keepercommander.importer.csv', - 'keepercommander.importer.keepass', - 'keepercommander.importer.lastpass', - 'keepercommander.plugins', - 'keepercommander.yubikey', - ], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - [], - exclude_binaries=True, - name='keeper', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=False, - console=True) -coll = COLLECT(exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='keeper') diff --git a/public_suffix_list.dat b/public_suffix_list.dat deleted file mode 100644 index eab989967..000000000 --- a/public_suffix_list.dat +++ /dev/null @@ -1,13756 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -// Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat, -// rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported. - -// Instructions on pulling and using this list can be found at https://publicsuffix.org/list/. - -// ===BEGIN ICANN DOMAINS=== - -// ac : http://nic.ac/rules.htm -ac -com.ac -edu.ac -gov.ac -net.ac -mil.ac -org.ac - -// ad : https://en.wikipedia.org/wiki/.ad -ad -nom.ad - -// ae : https://tdra.gov.ae/en/aeda/ae-policies -ae -co.ae -net.ae -org.ae -sch.ae -ac.ae -gov.ae -mil.ae - -// aero : see https://www.information.aero/index.php?id=66 -aero -accident-investigation.aero -accident-prevention.aero -aerobatic.aero -aeroclub.aero -aerodrome.aero -agents.aero -aircraft.aero -airline.aero -airport.aero -air-surveillance.aero -airtraffic.aero -air-traffic-control.aero -ambulance.aero -amusement.aero -association.aero -author.aero -ballooning.aero -broker.aero -caa.aero -cargo.aero -catering.aero -certification.aero -championship.aero -charter.aero -civilaviation.aero -club.aero -conference.aero -consultant.aero -consulting.aero -control.aero -council.aero -crew.aero -design.aero -dgca.aero -educator.aero -emergency.aero -engine.aero -engineer.aero -entertainment.aero -equipment.aero -exchange.aero -express.aero -federation.aero -flight.aero -fuel.aero -gliding.aero -government.aero -groundhandling.aero -group.aero -hanggliding.aero -homebuilt.aero -insurance.aero -journal.aero -journalist.aero -leasing.aero -logistics.aero -magazine.aero -maintenance.aero -media.aero -microlight.aero -modelling.aero -navigation.aero -parachuting.aero -paragliding.aero -passenger-association.aero -pilot.aero -press.aero -production.aero -recreation.aero -repbody.aero -res.aero -research.aero -rotorcraft.aero -safety.aero -scientist.aero -services.aero -show.aero -skydiving.aero -software.aero -student.aero -trader.aero -trading.aero -trainer.aero -union.aero -workinggroup.aero -works.aero - -// af : http://www.nic.af/help.jsp -af -gov.af -com.af -org.af -net.af -edu.af - -// ag : http://www.nic.ag/prices.htm -ag -com.ag -org.ag -net.ag -co.ag -nom.ag - -// ai : http://nic.com.ai/ -ai -off.ai -com.ai -net.ai -org.ai - -// al : http://www.ert.gov.al/ert_alb/faq_det.html?Id=31 -al -com.al -edu.al -gov.al -mil.al -net.al -org.al - -// am : https://www.amnic.net/policy/en/Policy_EN.pdf -am -co.am -com.am -commune.am -net.am -org.am - -// ao : https://en.wikipedia.org/wiki/.ao -// http://www.dns.ao/REGISTR.DOC -ao -ed.ao -gv.ao -og.ao -co.ao -pb.ao -it.ao - -// aq : https://en.wikipedia.org/wiki/.aq -aq - -// ar : https://nic.ar/es/nic-argentina/normativa -ar -bet.ar -com.ar -coop.ar -edu.ar -gob.ar -gov.ar -int.ar -mil.ar -musica.ar -mutual.ar -net.ar -org.ar -senasa.ar -tur.ar - -// arpa : https://en.wikipedia.org/wiki/.arpa -// Confirmed by registry 2008-06-18 -arpa -e164.arpa -in-addr.arpa -ip6.arpa -iris.arpa -uri.arpa -urn.arpa - -// as : https://en.wikipedia.org/wiki/.as -as -gov.as - -// asia : https://en.wikipedia.org/wiki/.asia -asia - -// at : https://en.wikipedia.org/wiki/.at -// Confirmed by registry 2008-06-17 -at -ac.at -co.at -gv.at -or.at -sth.ac.at - -// au : https://en.wikipedia.org/wiki/.au -// http://www.auda.org.au/ -au -// 2LDs -com.au -net.au -org.au -edu.au -gov.au -asn.au -id.au -// Historic 2LDs (closed to new registration, but sites still exist) -info.au -conf.au -oz.au -// CGDNs - http://www.cgdn.org.au/ -act.au -nsw.au -nt.au -qld.au -sa.au -tas.au -vic.au -wa.au -// 3LDs -act.edu.au -catholic.edu.au -// eq.edu.au - Removed at the request of the Queensland Department of Education -nsw.edu.au -nt.edu.au -qld.edu.au -sa.edu.au -tas.edu.au -vic.edu.au -wa.edu.au -// act.gov.au Bug 984824 - Removed at request of Greg Tankard -// nsw.gov.au Bug 547985 - Removed at request of -// nt.gov.au Bug 940478 - Removed at request of Greg Connors -qld.gov.au -sa.gov.au -tas.gov.au -vic.gov.au -wa.gov.au -// 4LDs -// education.tas.edu.au - Removed at the request of the Department of Education Tasmania -schools.nsw.edu.au - -// aw : https://en.wikipedia.org/wiki/.aw -aw -com.aw - -// ax : https://en.wikipedia.org/wiki/.ax -ax - -// az : https://en.wikipedia.org/wiki/.az -az -com.az -net.az -int.az -gov.az -org.az -edu.az -info.az -pp.az -mil.az -name.az -pro.az -biz.az - -// ba : http://nic.ba/users_data/files/pravilnik_o_registraciji.pdf -ba -com.ba -edu.ba -gov.ba -mil.ba -net.ba -org.ba - -// bb : https://en.wikipedia.org/wiki/.bb -bb -biz.bb -co.bb -com.bb -edu.bb -gov.bb -info.bb -net.bb -org.bb -store.bb -tv.bb - -// bd : https://en.wikipedia.org/wiki/.bd -*.bd - -// be : https://en.wikipedia.org/wiki/.be -// Confirmed by registry 2008-06-08 -be -ac.be - -// bf : https://en.wikipedia.org/wiki/.bf -bf -gov.bf - -// bg : https://en.wikipedia.org/wiki/.bg -// https://www.register.bg/user/static/rules/en/index.html -bg -a.bg -b.bg -c.bg -d.bg -e.bg -f.bg -g.bg -h.bg -i.bg -j.bg -k.bg -l.bg -m.bg -n.bg -o.bg -p.bg -q.bg -r.bg -s.bg -t.bg -u.bg -v.bg -w.bg -x.bg -y.bg -z.bg -0.bg -1.bg -2.bg -3.bg -4.bg -5.bg -6.bg -7.bg -8.bg -9.bg - -// bh : https://en.wikipedia.org/wiki/.bh -bh -com.bh -edu.bh -net.bh -org.bh -gov.bh - -// bi : https://en.wikipedia.org/wiki/.bi -// http://whois.nic.bi/ -bi -co.bi -com.bi -edu.bi -or.bi -org.bi - -// biz : https://en.wikipedia.org/wiki/.biz -biz - -// bj : https://nic.bj/bj-suffixes.txt -// submitted by registry -bj -africa.bj -agro.bj -architectes.bj -assur.bj -avocats.bj -co.bj -com.bj -eco.bj -econo.bj -edu.bj -info.bj -loisirs.bj -money.bj -net.bj -org.bj -ote.bj -resto.bj -restaurant.bj -tourism.bj -univ.bj - -// bm : http://www.bermudanic.bm/dnr-text.txt -bm -com.bm -edu.bm -gov.bm -net.bm -org.bm - -// bn : http://www.bnnic.bn/faqs -bn -com.bn -edu.bn -gov.bn -net.bn -org.bn - -// bo : https://nic.bo/delegacion2015.php#h-1.10 -bo -com.bo -edu.bo -gob.bo -int.bo -org.bo -net.bo -mil.bo -tv.bo -web.bo -// Social Domains -academia.bo -agro.bo -arte.bo -blog.bo -bolivia.bo -ciencia.bo -cooperativa.bo -democracia.bo -deporte.bo -ecologia.bo -economia.bo -empresa.bo -indigena.bo -industria.bo -info.bo -medicina.bo -movimiento.bo -musica.bo -natural.bo -nombre.bo -noticias.bo -patria.bo -politica.bo -profesional.bo -plurinacional.bo -pueblo.bo -revista.bo -salud.bo -tecnologia.bo -tksat.bo -transporte.bo -wiki.bo - -// br : http://registro.br/dominio/categoria.html -// Submitted by registry -br -9guacu.br -abc.br -adm.br -adv.br -agr.br -aju.br -am.br -anani.br -aparecida.br -app.br -arq.br -art.br -ato.br -b.br -barueri.br -belem.br -bhz.br -bib.br -bio.br -blog.br -bmd.br -boavista.br -bsb.br -campinagrande.br -campinas.br -caxias.br -cim.br -cng.br -cnt.br -com.br -contagem.br -coop.br -coz.br -cri.br -cuiaba.br -curitiba.br -def.br -des.br -det.br -dev.br -ecn.br -eco.br -edu.br -emp.br -enf.br -eng.br -esp.br -etc.br -eti.br -far.br -feira.br -flog.br -floripa.br -fm.br -fnd.br -fortal.br -fot.br -foz.br -fst.br -g12.br -geo.br -ggf.br -goiania.br -gov.br -// gov.br 26 states + df https://en.wikipedia.org/wiki/States_of_Brazil -ac.gov.br -al.gov.br -am.gov.br -ap.gov.br -ba.gov.br -ce.gov.br -df.gov.br -es.gov.br -go.gov.br -ma.gov.br -mg.gov.br -ms.gov.br -mt.gov.br -pa.gov.br -pb.gov.br -pe.gov.br -pi.gov.br -pr.gov.br -rj.gov.br -rn.gov.br -ro.gov.br -rr.gov.br -rs.gov.br -sc.gov.br -se.gov.br -sp.gov.br -to.gov.br -gru.br -imb.br -ind.br -inf.br -jab.br -jampa.br -jdf.br -joinville.br -jor.br -jus.br -leg.br -lel.br -log.br -londrina.br -macapa.br -maceio.br -manaus.br -maringa.br -mat.br -med.br -mil.br -morena.br -mp.br -mus.br -natal.br -net.br -niteroi.br -*.nom.br -not.br -ntr.br -odo.br -ong.br -org.br -osasco.br -palmas.br -poa.br -ppg.br -pro.br -psc.br -psi.br -pvh.br -qsl.br -radio.br -rec.br -recife.br -rep.br -ribeirao.br -rio.br -riobranco.br -riopreto.br -salvador.br -sampa.br -santamaria.br -santoandre.br -saobernardo.br -saogonca.br -seg.br -sjc.br -slg.br -slz.br -sorocaba.br -srv.br -taxi.br -tc.br -tec.br -teo.br -the.br -tmp.br -trd.br -tur.br -tv.br -udi.br -vet.br -vix.br -vlog.br -wiki.br -zlg.br - -// bs : http://www.nic.bs/rules.html -bs -com.bs -net.bs -org.bs -edu.bs -gov.bs - -// bt : https://en.wikipedia.org/wiki/.bt -bt -com.bt -edu.bt -gov.bt -net.bt -org.bt - -// bv : No registrations at this time. -// Submitted by registry -bv - -// bw : https://en.wikipedia.org/wiki/.bw -// http://www.gobin.info/domainname/bw.doc -// list of other 2nd level tlds ? -bw -co.bw -org.bw - -// by : https://en.wikipedia.org/wiki/.by -// http://tld.by/rules_2006_en.html -// list of other 2nd level tlds ? -by -gov.by -mil.by -// Official information does not indicate that com.by is a reserved -// second-level domain, but it's being used as one (see www.google.com.by and -// www.yahoo.com.by, for example), so we list it here for safety's sake. -com.by - -// http://hoster.by/ -of.by - -// bz : https://en.wikipedia.org/wiki/.bz -// http://www.belizenic.bz/ -bz -com.bz -net.bz -org.bz -edu.bz -gov.bz - -// ca : https://en.wikipedia.org/wiki/.ca -ca -// ca geographical names -ab.ca -bc.ca -mb.ca -nb.ca -nf.ca -nl.ca -ns.ca -nt.ca -nu.ca -on.ca -pe.ca -qc.ca -sk.ca -yk.ca -// gc.ca: https://en.wikipedia.org/wiki/.gc.ca -// see also: http://registry.gc.ca/en/SubdomainFAQ -gc.ca - -// cat : https://en.wikipedia.org/wiki/.cat -cat - -// cc : https://en.wikipedia.org/wiki/.cc -cc - -// cd : https://en.wikipedia.org/wiki/.cd -// see also: https://www.nic.cd/domain/insertDomain_2.jsp?act=1 -cd -gov.cd - -// cf : https://en.wikipedia.org/wiki/.cf -cf - -// cg : https://en.wikipedia.org/wiki/.cg -cg - -// ch : https://en.wikipedia.org/wiki/.ch -ch - -// ci : https://en.wikipedia.org/wiki/.ci -// http://www.nic.ci/index.php?page=charte -ci -org.ci -or.ci -com.ci -co.ci -edu.ci -ed.ci -ac.ci -net.ci -go.ci -asso.ci -aéroport.ci -int.ci -presse.ci -md.ci -gouv.ci - -// ck : https://en.wikipedia.org/wiki/.ck -*.ck -!www.ck - -// cl : https://www.nic.cl -// Confirmed by .CL registry -cl -co.cl -gob.cl -gov.cl -mil.cl - -// cm : https://en.wikipedia.org/wiki/.cm plus bug 981927 -cm -co.cm -com.cm -gov.cm -net.cm - -// cn : https://en.wikipedia.org/wiki/.cn -// Submitted by registry -cn -ac.cn -com.cn -edu.cn -gov.cn -net.cn -org.cn -mil.cn -公司.cn -网络.cn -網絡.cn -// cn geographic names -ah.cn -bj.cn -cq.cn -fj.cn -gd.cn -gs.cn -gz.cn -gx.cn -ha.cn -hb.cn -he.cn -hi.cn -hl.cn -hn.cn -jl.cn -js.cn -jx.cn -ln.cn -nm.cn -nx.cn -qh.cn -sc.cn -sd.cn -sh.cn -sn.cn -sx.cn -tj.cn -xj.cn -xz.cn -yn.cn -zj.cn -hk.cn -mo.cn -tw.cn - -// co : https://en.wikipedia.org/wiki/.co -// Submitted by registry -co -arts.co -com.co -edu.co -firm.co -gov.co -info.co -int.co -mil.co -net.co -nom.co -org.co -rec.co -web.co - -// com : https://en.wikipedia.org/wiki/.com -com - -// coop : https://en.wikipedia.org/wiki/.coop -coop - -// cr : http://www.nic.cr/niccr_publico/showRegistroDominiosScreen.do -cr -ac.cr -co.cr -ed.cr -fi.cr -go.cr -or.cr -sa.cr - -// cu : https://en.wikipedia.org/wiki/.cu -cu -com.cu -edu.cu -org.cu -net.cu -gov.cu -inf.cu - -// cv : https://en.wikipedia.org/wiki/.cv -// cv : http://www.dns.cv/tldcv_portal/do?com=DS;5446457100;111;+PAGE(4000018)+K-CAT-CODIGO(RDOM)+RCNT(100); <- registration rules -cv -com.cv -edu.cv -int.cv -nome.cv -org.cv - -// cw : http://www.una.cw/cw_registry/ -// Confirmed by registry 2013-03-26 -cw -com.cw -edu.cw -net.cw -org.cw - -// cx : https://en.wikipedia.org/wiki/.cx -// list of other 2nd level tlds ? -cx -gov.cx - -// cy : http://www.nic.cy/ -// Submitted by registry Panayiotou Fotia -// namespace policies URL https://www.nic.cy/portal//sites/default/files/symfonia_gia_eggrafi.pdf -cy -ac.cy -biz.cy -com.cy -ekloges.cy -gov.cy -ltd.cy -mil.cy -net.cy -org.cy -press.cy -pro.cy -tm.cy - -// cz : https://en.wikipedia.org/wiki/.cz -cz - -// de : https://en.wikipedia.org/wiki/.de -// Confirmed by registry (with technical -// reservations) 2008-07-01 -de - -// dj : https://en.wikipedia.org/wiki/.dj -dj - -// dk : https://en.wikipedia.org/wiki/.dk -// Confirmed by registry 2008-06-17 -dk - -// dm : https://en.wikipedia.org/wiki/.dm -dm -com.dm -net.dm -org.dm -edu.dm -gov.dm - -// do : https://en.wikipedia.org/wiki/.do -do -art.do -com.do -edu.do -gob.do -gov.do -mil.do -net.do -org.do -sld.do -web.do - -// dz : http://www.nic.dz/images/pdf_nic/charte.pdf -dz -art.dz -asso.dz -com.dz -edu.dz -gov.dz -org.dz -net.dz -pol.dz -soc.dz -tm.dz - -// ec : http://www.nic.ec/reg/paso1.asp -// Submitted by registry -ec -com.ec -info.ec -net.ec -fin.ec -k12.ec -med.ec -pro.ec -org.ec -edu.ec -gov.ec -gob.ec -mil.ec - -// edu : https://en.wikipedia.org/wiki/.edu -edu - -// ee : http://www.eenet.ee/EENet/dom_reeglid.html#lisa_B -ee -edu.ee -gov.ee -riik.ee -lib.ee -med.ee -com.ee -pri.ee -aip.ee -org.ee -fie.ee - -// eg : https://en.wikipedia.org/wiki/.eg -eg -com.eg -edu.eg -eun.eg -gov.eg -mil.eg -name.eg -net.eg -org.eg -sci.eg - -// er : https://en.wikipedia.org/wiki/.er -*.er - -// es : https://www.nic.es/site_ingles/ingles/dominios/index.html -es -com.es -nom.es -org.es -gob.es -edu.es - -// et : https://en.wikipedia.org/wiki/.et -et -com.et -gov.et -org.et -edu.et -biz.et -name.et -info.et -net.et - -// eu : https://en.wikipedia.org/wiki/.eu -eu - -// fi : https://en.wikipedia.org/wiki/.fi -fi -// aland.fi : https://en.wikipedia.org/wiki/.ax -// This domain is being phased out in favor of .ax. As there are still many -// domains under aland.fi, we still keep it on the list until aland.fi is -// completely removed. -// TODO: Check for updates (expected to be phased out around Q1/2009) -aland.fi - -// fj : http://domains.fj/ -// Submitted by registry 2020-02-11 -fj -ac.fj -biz.fj -com.fj -gov.fj -info.fj -mil.fj -name.fj -net.fj -org.fj -pro.fj - -// fk : https://en.wikipedia.org/wiki/.fk -*.fk - -// fm : https://en.wikipedia.org/wiki/.fm -com.fm -edu.fm -net.fm -org.fm -fm - -// fo : https://en.wikipedia.org/wiki/.fo -fo - -// fr : http://www.afnic.fr/ -// domaines descriptifs : https://www.afnic.fr/medias/documents/Cadre_legal/Afnic_Naming_Policy_12122016_VEN.pdf -fr -asso.fr -com.fr -gouv.fr -nom.fr -prd.fr -tm.fr -// domaines sectoriels : https://www.afnic.fr/en/products-and-services/the-fr-tld/sector-based-fr-domains-4.html -aeroport.fr -avocat.fr -avoues.fr -cci.fr -chambagri.fr -chirurgiens-dentistes.fr -experts-comptables.fr -geometre-expert.fr -greta.fr -huissier-justice.fr -medecin.fr -notaires.fr -pharmacien.fr -port.fr -veterinaire.fr - -// ga : https://en.wikipedia.org/wiki/.ga -ga - -// gb : This registry is effectively dormant -// Submitted by registry -gb - -// gd : https://en.wikipedia.org/wiki/.gd -edu.gd -gov.gd -gd - -// ge : http://www.nic.net.ge/policy_en.pdf -ge -com.ge -edu.ge -gov.ge -org.ge -mil.ge -net.ge -pvt.ge - -// gf : https://en.wikipedia.org/wiki/.gf -gf - -// gg : http://www.channelisles.net/register-domains/ -// Confirmed by registry 2013-11-28 -gg -co.gg -net.gg -org.gg - -// gh : https://en.wikipedia.org/wiki/.gh -// see also: http://www.nic.gh/reg_now.php -// Although domains directly at second level are not possible at the moment, -// they have been possible for some time and may come back. -gh -com.gh -edu.gh -gov.gh -org.gh -mil.gh - -// gi : http://www.nic.gi/rules.html -gi -com.gi -ltd.gi -gov.gi -mod.gi -edu.gi -org.gi - -// gl : https://en.wikipedia.org/wiki/.gl -// http://nic.gl -gl -co.gl -com.gl -edu.gl -net.gl -org.gl - -// gm : http://www.nic.gm/htmlpages%5Cgm-policy.htm -gm - -// gn : http://psg.com/dns/gn/gn.txt -// Submitted by registry -gn -ac.gn -com.gn -edu.gn -gov.gn -org.gn -net.gn - -// gov : https://en.wikipedia.org/wiki/.gov -gov - -// gp : http://www.nic.gp/index.php?lang=en -gp -com.gp -net.gp -mobi.gp -edu.gp -org.gp -asso.gp - -// gq : https://en.wikipedia.org/wiki/.gq -gq - -// gr : https://grweb.ics.forth.gr/english/1617-B-2005.html -// Submitted by registry -gr -com.gr -edu.gr -net.gr -org.gr -gov.gr - -// gs : https://en.wikipedia.org/wiki/.gs -gs - -// gt : https://www.gt/sitio/registration_policy.php?lang=en -gt -com.gt -edu.gt -gob.gt -ind.gt -mil.gt -net.gt -org.gt - -// gu : http://gadao.gov.gu/register.html -// University of Guam : https://www.uog.edu -// Submitted by uognoc@triton.uog.edu -gu -com.gu -edu.gu -gov.gu -guam.gu -info.gu -net.gu -org.gu -web.gu - -// gw : https://en.wikipedia.org/wiki/.gw -// gw : https://nic.gw/regras/ -gw - -// gy : https://en.wikipedia.org/wiki/.gy -// http://registry.gy/ -gy -co.gy -com.gy -edu.gy -gov.gy -net.gy -org.gy - -// hk : https://www.hkirc.hk -// Submitted by registry -hk -com.hk -edu.hk -gov.hk -idv.hk -net.hk -org.hk -公司.hk -教育.hk -敎育.hk -政府.hk -個人.hk -个人.hk -箇人.hk -網络.hk -网络.hk -组織.hk -網絡.hk -网絡.hk -组织.hk -組織.hk -組织.hk - -// hm : https://en.wikipedia.org/wiki/.hm -hm - -// hn : http://www.nic.hn/politicas/ps02,,05.html -hn -com.hn -edu.hn -org.hn -net.hn -mil.hn -gob.hn - -// hr : http://www.dns.hr/documents/pdf/HRTLD-regulations.pdf -hr -iz.hr -from.hr -name.hr -com.hr - -// ht : http://www.nic.ht/info/charte.cfm -ht -com.ht -shop.ht -firm.ht -info.ht -adult.ht -net.ht -pro.ht -org.ht -med.ht -art.ht -coop.ht -pol.ht -asso.ht -edu.ht -rel.ht -gouv.ht -perso.ht - -// hu : http://www.domain.hu/domain/English/sld.html -// Confirmed by registry 2008-06-12 -hu -co.hu -info.hu -org.hu -priv.hu -sport.hu -tm.hu -2000.hu -agrar.hu -bolt.hu -casino.hu -city.hu -erotica.hu -erotika.hu -film.hu -forum.hu -games.hu -hotel.hu -ingatlan.hu -jogasz.hu -konyvelo.hu -lakas.hu -media.hu -news.hu -reklam.hu -sex.hu -shop.hu -suli.hu -szex.hu -tozsde.hu -utazas.hu -video.hu - -// id : https://pandi.id/en/domain/registration-requirements/ -id -ac.id -biz.id -co.id -desa.id -go.id -mil.id -my.id -net.id -or.id -ponpes.id -sch.id -web.id - -// ie : https://en.wikipedia.org/wiki/.ie -ie -gov.ie - -// il : http://www.isoc.org.il/domains/ -// see also: https://en.isoc.org.il/il-cctld/registration-rules -// ISOC-IL (operated by .il Registry) -il -ac.il -co.il -gov.il -idf.il -k12.il -muni.il -net.il -org.il -// xn--4dbrk0ce ("Israel", Hebrew) : IL -ישראל -// xn--4dbgdty6c.xn--4dbrk0ce. -אקדמיה.ישראל -// xn--5dbhl8d.xn--4dbrk0ce. -ישוב.ישראל -// xn--8dbq2a.xn--4dbrk0ce. -צהל.ישראל -// xn--hebda8b.xn--4dbrk0ce. -ממשל.ישראל - -// im : https://www.nic.im/ -// Submitted by registry -im -ac.im -co.im -com.im -ltd.co.im -net.im -org.im -plc.co.im -tt.im -tv.im - -// in : https://en.wikipedia.org/wiki/.in -// see also: https://registry.in/policies -// Please note, that nic.in is not an official eTLD, but used by most -// government institutions. -in -5g.in -6g.in -ac.in -ai.in -am.in -bihar.in -biz.in -business.in -ca.in -cn.in -co.in -com.in -coop.in -cs.in -delhi.in -dr.in -edu.in -er.in -firm.in -gen.in -gov.in -gujarat.in -ind.in -info.in -int.in -internet.in -io.in -me.in -mil.in -net.in -nic.in -org.in -pg.in -post.in -pro.in -res.in -travel.in -tv.in -uk.in -up.in -us.in - -// info : https://en.wikipedia.org/wiki/.info -info - -// int : https://en.wikipedia.org/wiki/.int -// Confirmed by registry 2008-06-18 -int -eu.int - -// io : http://www.nic.io/rules.htm -// list of other 2nd level tlds ? -io -com.io - -// iq : http://www.cmc.iq/english/iq/iqregister1.htm -iq -gov.iq -edu.iq -mil.iq -com.iq -org.iq -net.iq - -// ir : http://www.nic.ir/Terms_and_Conditions_ir,_Appendix_1_Domain_Rules -// Also see http://www.nic.ir/Internationalized_Domain_Names -// Two .ir entries added at request of , 2010-04-16 -ir -ac.ir -co.ir -gov.ir -id.ir -net.ir -org.ir -sch.ir -// xn--mgba3a4f16a.ir (.ir, Persian YEH) -ایران.ir -// xn--mgba3a4fra.ir (.ir, Arabic YEH) -ايران.ir - -// is : http://www.isnic.is/domain/rules.php -// Confirmed by registry 2008-12-06 -is -net.is -com.is -edu.is -gov.is -org.is -int.is - -// it : https://en.wikipedia.org/wiki/.it -it -gov.it -edu.it -// Reserved geo-names (regions and provinces): -// https://www.nic.it/sites/default/files/archivio/docs/Regulation_assignation_v7.1.pdf -// Regions -abr.it -abruzzo.it -aosta-valley.it -aostavalley.it -bas.it -basilicata.it -cal.it -calabria.it -cam.it -campania.it -emilia-romagna.it -emiliaromagna.it -emr.it -friuli-v-giulia.it -friuli-ve-giulia.it -friuli-vegiulia.it -friuli-venezia-giulia.it -friuli-veneziagiulia.it -friuli-vgiulia.it -friuliv-giulia.it -friulive-giulia.it -friulivegiulia.it -friulivenezia-giulia.it -friuliveneziagiulia.it -friulivgiulia.it -fvg.it -laz.it -lazio.it -lig.it -liguria.it -lom.it -lombardia.it -lombardy.it -lucania.it -mar.it -marche.it -mol.it -molise.it -piedmont.it -piemonte.it -pmn.it -pug.it -puglia.it -sar.it -sardegna.it -sardinia.it -sic.it -sicilia.it -sicily.it -taa.it -tos.it -toscana.it -trentin-sud-tirol.it -trentin-süd-tirol.it -trentin-sudtirol.it -trentin-südtirol.it -trentin-sued-tirol.it -trentin-suedtirol.it -trentino-a-adige.it -trentino-aadige.it -trentino-alto-adige.it -trentino-altoadige.it -trentino-s-tirol.it -trentino-stirol.it -trentino-sud-tirol.it -trentino-süd-tirol.it -trentino-sudtirol.it -trentino-südtirol.it -trentino-sued-tirol.it -trentino-suedtirol.it -trentino.it -trentinoa-adige.it -trentinoaadige.it -trentinoalto-adige.it -trentinoaltoadige.it -trentinos-tirol.it -trentinostirol.it -trentinosud-tirol.it -trentinosüd-tirol.it -trentinosudtirol.it -trentinosüdtirol.it -trentinosued-tirol.it -trentinosuedtirol.it -trentinsud-tirol.it -trentinsüd-tirol.it -trentinsudtirol.it -trentinsüdtirol.it -trentinsued-tirol.it -trentinsuedtirol.it -tuscany.it -umb.it -umbria.it -val-d-aosta.it -val-daosta.it -vald-aosta.it -valdaosta.it -valle-aosta.it -valle-d-aosta.it -valle-daosta.it -valleaosta.it -valled-aosta.it -valledaosta.it -vallee-aoste.it -vallée-aoste.it -vallee-d-aoste.it -vallée-d-aoste.it -valleeaoste.it -valléeaoste.it -valleedaoste.it -valléedaoste.it -vao.it -vda.it -ven.it -veneto.it -// Provinces -ag.it -agrigento.it -al.it -alessandria.it -alto-adige.it -altoadige.it -an.it -ancona.it -andria-barletta-trani.it -andria-trani-barletta.it -andriabarlettatrani.it -andriatranibarletta.it -ao.it -aosta.it -aoste.it -ap.it -aq.it -aquila.it -ar.it -arezzo.it -ascoli-piceno.it -ascolipiceno.it -asti.it -at.it -av.it -avellino.it -ba.it -balsan-sudtirol.it -balsan-südtirol.it -balsan-suedtirol.it -balsan.it -bari.it -barletta-trani-andria.it -barlettatraniandria.it -belluno.it -benevento.it -bergamo.it -bg.it -bi.it -biella.it -bl.it -bn.it -bo.it -bologna.it -bolzano-altoadige.it -bolzano.it -bozen-sudtirol.it -bozen-südtirol.it -bozen-suedtirol.it -bozen.it -br.it -brescia.it -brindisi.it -bs.it -bt.it -bulsan-sudtirol.it -bulsan-südtirol.it -bulsan-suedtirol.it -bulsan.it -bz.it -ca.it -cagliari.it -caltanissetta.it -campidano-medio.it -campidanomedio.it -campobasso.it -carbonia-iglesias.it -carboniaiglesias.it -carrara-massa.it -carraramassa.it -caserta.it -catania.it -catanzaro.it -cb.it -ce.it -cesena-forli.it -cesena-forlì.it -cesenaforli.it -cesenaforlì.it -ch.it -chieti.it -ci.it -cl.it -cn.it -co.it -como.it -cosenza.it -cr.it -cremona.it -crotone.it -cs.it -ct.it -cuneo.it -cz.it -dell-ogliastra.it -dellogliastra.it -en.it -enna.it -fc.it -fe.it -fermo.it -ferrara.it -fg.it -fi.it -firenze.it -florence.it -fm.it -foggia.it -forli-cesena.it -forlì-cesena.it -forlicesena.it -forlìcesena.it -fr.it -frosinone.it -ge.it -genoa.it -genova.it -go.it -gorizia.it -gr.it -grosseto.it -iglesias-carbonia.it -iglesiascarbonia.it -im.it -imperia.it -is.it -isernia.it -kr.it -la-spezia.it -laquila.it -laspezia.it -latina.it -lc.it -le.it -lecce.it -lecco.it -li.it -livorno.it -lo.it -lodi.it -lt.it -lu.it -lucca.it -macerata.it -mantova.it -massa-carrara.it -massacarrara.it -matera.it -mb.it -mc.it -me.it -medio-campidano.it -mediocampidano.it -messina.it -mi.it -milan.it -milano.it -mn.it -mo.it -modena.it -monza-brianza.it -monza-e-della-brianza.it -monza.it -monzabrianza.it -monzaebrianza.it -monzaedellabrianza.it -ms.it -mt.it -na.it -naples.it -napoli.it -no.it -novara.it -nu.it -nuoro.it -og.it -ogliastra.it -olbia-tempio.it -olbiatempio.it -or.it -oristano.it -ot.it -pa.it -padova.it -padua.it -palermo.it -parma.it -pavia.it -pc.it -pd.it -pe.it -perugia.it -pesaro-urbino.it -pesarourbino.it -pescara.it -pg.it -pi.it -piacenza.it -pisa.it -pistoia.it -pn.it -po.it -pordenone.it -potenza.it -pr.it -prato.it -pt.it -pu.it -pv.it -pz.it -ra.it -ragusa.it -ravenna.it -rc.it -re.it -reggio-calabria.it -reggio-emilia.it -reggiocalabria.it -reggioemilia.it -rg.it -ri.it -rieti.it -rimini.it -rm.it -rn.it -ro.it -roma.it -rome.it -rovigo.it -sa.it -salerno.it -sassari.it -savona.it -si.it -siena.it -siracusa.it -so.it -sondrio.it -sp.it -sr.it -ss.it -suedtirol.it -südtirol.it -sv.it -ta.it -taranto.it -te.it -tempio-olbia.it -tempioolbia.it -teramo.it -terni.it -tn.it -to.it -torino.it -tp.it -tr.it -trani-andria-barletta.it -trani-barletta-andria.it -traniandriabarletta.it -tranibarlettaandria.it -trapani.it -trento.it -treviso.it -trieste.it -ts.it -turin.it -tv.it -ud.it -udine.it -urbino-pesaro.it -urbinopesaro.it -va.it -varese.it -vb.it -vc.it -ve.it -venezia.it -venice.it -verbania.it -vercelli.it -verona.it -vi.it -vibo-valentia.it -vibovalentia.it -vicenza.it -viterbo.it -vr.it -vs.it -vt.it -vv.it - -// je : http://www.channelisles.net/register-domains/ -// Confirmed by registry 2013-11-28 -je -co.je -net.je -org.je - -// jm : http://www.com.jm/register.html -*.jm - -// jo : http://www.dns.jo/Registration_policy.aspx -jo -com.jo -org.jo -net.jo -edu.jo -sch.jo -gov.jo -mil.jo -name.jo - -// jobs : https://en.wikipedia.org/wiki/.jobs -jobs - -// jp : https://en.wikipedia.org/wiki/.jp -// http://jprs.co.jp/en/jpdomain.html -// Submitted by registry -jp -// jp organizational type names -ac.jp -ad.jp -co.jp -ed.jp -go.jp -gr.jp -lg.jp -ne.jp -or.jp -// jp prefecture type names -aichi.jp -akita.jp -aomori.jp -chiba.jp -ehime.jp -fukui.jp -fukuoka.jp -fukushima.jp -gifu.jp -gunma.jp -hiroshima.jp -hokkaido.jp -hyogo.jp -ibaraki.jp -ishikawa.jp -iwate.jp -kagawa.jp -kagoshima.jp -kanagawa.jp -kochi.jp -kumamoto.jp -kyoto.jp -mie.jp -miyagi.jp -miyazaki.jp -nagano.jp -nagasaki.jp -nara.jp -niigata.jp -oita.jp -okayama.jp -okinawa.jp -osaka.jp -saga.jp -saitama.jp -shiga.jp -shimane.jp -shizuoka.jp -tochigi.jp -tokushima.jp -tokyo.jp -tottori.jp -toyama.jp -wakayama.jp -yamagata.jp -yamaguchi.jp -yamanashi.jp -栃木.jp -愛知.jp -愛媛.jp -兵庫.jp -熊本.jp -茨城.jp -北海道.jp -千葉.jp -和歌山.jp -長崎.jp -長野.jp -新潟.jp -青森.jp -静岡.jp -東京.jp -石川.jp -埼玉.jp -三重.jp -京都.jp -佐賀.jp -大分.jp -大阪.jp -奈良.jp -宮城.jp -宮崎.jp -富山.jp -山口.jp -山形.jp -山梨.jp -岩手.jp -岐阜.jp -岡山.jp -島根.jp -広島.jp -徳島.jp -沖縄.jp -滋賀.jp -神奈川.jp -福井.jp -福岡.jp -福島.jp -秋田.jp -群馬.jp -香川.jp -高知.jp -鳥取.jp -鹿児島.jp -// jp geographic type names -// http://jprs.jp/doc/rule/saisoku-1.html -*.kawasaki.jp -*.kitakyushu.jp -*.kobe.jp -*.nagoya.jp -*.sapporo.jp -*.sendai.jp -*.yokohama.jp -!city.kawasaki.jp -!city.kitakyushu.jp -!city.kobe.jp -!city.nagoya.jp -!city.sapporo.jp -!city.sendai.jp -!city.yokohama.jp -// 4th level registration -aisai.aichi.jp -ama.aichi.jp -anjo.aichi.jp -asuke.aichi.jp -chiryu.aichi.jp -chita.aichi.jp -fuso.aichi.jp -gamagori.aichi.jp -handa.aichi.jp -hazu.aichi.jp -hekinan.aichi.jp -higashiura.aichi.jp -ichinomiya.aichi.jp -inazawa.aichi.jp -inuyama.aichi.jp -isshiki.aichi.jp -iwakura.aichi.jp -kanie.aichi.jp -kariya.aichi.jp -kasugai.aichi.jp -kira.aichi.jp -kiyosu.aichi.jp -komaki.aichi.jp -konan.aichi.jp -kota.aichi.jp -mihama.aichi.jp -miyoshi.aichi.jp -nishio.aichi.jp -nisshin.aichi.jp -obu.aichi.jp -oguchi.aichi.jp -oharu.aichi.jp -okazaki.aichi.jp -owariasahi.aichi.jp -seto.aichi.jp -shikatsu.aichi.jp -shinshiro.aichi.jp -shitara.aichi.jp -tahara.aichi.jp -takahama.aichi.jp -tobishima.aichi.jp -toei.aichi.jp -togo.aichi.jp -tokai.aichi.jp -tokoname.aichi.jp -toyoake.aichi.jp -toyohashi.aichi.jp -toyokawa.aichi.jp -toyone.aichi.jp -toyota.aichi.jp -tsushima.aichi.jp -yatomi.aichi.jp -akita.akita.jp -daisen.akita.jp -fujisato.akita.jp -gojome.akita.jp -hachirogata.akita.jp -happou.akita.jp -higashinaruse.akita.jp -honjo.akita.jp -honjyo.akita.jp -ikawa.akita.jp -kamikoani.akita.jp -kamioka.akita.jp -katagami.akita.jp -kazuno.akita.jp -kitaakita.akita.jp -kosaka.akita.jp -kyowa.akita.jp -misato.akita.jp -mitane.akita.jp -moriyoshi.akita.jp -nikaho.akita.jp -noshiro.akita.jp -odate.akita.jp -oga.akita.jp -ogata.akita.jp -semboku.akita.jp -yokote.akita.jp -yurihonjo.akita.jp -aomori.aomori.jp -gonohe.aomori.jp -hachinohe.aomori.jp -hashikami.aomori.jp -hiranai.aomori.jp -hirosaki.aomori.jp -itayanagi.aomori.jp -kuroishi.aomori.jp -misawa.aomori.jp -mutsu.aomori.jp -nakadomari.aomori.jp -noheji.aomori.jp -oirase.aomori.jp -owani.aomori.jp -rokunohe.aomori.jp -sannohe.aomori.jp -shichinohe.aomori.jp -shingo.aomori.jp -takko.aomori.jp -towada.aomori.jp -tsugaru.aomori.jp -tsuruta.aomori.jp -abiko.chiba.jp -asahi.chiba.jp -chonan.chiba.jp -chosei.chiba.jp -choshi.chiba.jp -chuo.chiba.jp -funabashi.chiba.jp -futtsu.chiba.jp -hanamigawa.chiba.jp -ichihara.chiba.jp -ichikawa.chiba.jp -ichinomiya.chiba.jp -inzai.chiba.jp -isumi.chiba.jp -kamagaya.chiba.jp -kamogawa.chiba.jp -kashiwa.chiba.jp -katori.chiba.jp -katsuura.chiba.jp -kimitsu.chiba.jp -kisarazu.chiba.jp -kozaki.chiba.jp -kujukuri.chiba.jp -kyonan.chiba.jp -matsudo.chiba.jp -midori.chiba.jp -mihama.chiba.jp -minamiboso.chiba.jp -mobara.chiba.jp -mutsuzawa.chiba.jp -nagara.chiba.jp -nagareyama.chiba.jp -narashino.chiba.jp -narita.chiba.jp -noda.chiba.jp -oamishirasato.chiba.jp -omigawa.chiba.jp -onjuku.chiba.jp -otaki.chiba.jp -sakae.chiba.jp -sakura.chiba.jp -shimofusa.chiba.jp -shirako.chiba.jp -shiroi.chiba.jp -shisui.chiba.jp -sodegaura.chiba.jp -sosa.chiba.jp -tako.chiba.jp -tateyama.chiba.jp -togane.chiba.jp -tohnosho.chiba.jp -tomisato.chiba.jp -urayasu.chiba.jp -yachimata.chiba.jp -yachiyo.chiba.jp -yokaichiba.chiba.jp -yokoshibahikari.chiba.jp -yotsukaido.chiba.jp -ainan.ehime.jp -honai.ehime.jp -ikata.ehime.jp -imabari.ehime.jp -iyo.ehime.jp -kamijima.ehime.jp -kihoku.ehime.jp -kumakogen.ehime.jp -masaki.ehime.jp -matsuno.ehime.jp -matsuyama.ehime.jp -namikata.ehime.jp -niihama.ehime.jp -ozu.ehime.jp -saijo.ehime.jp -seiyo.ehime.jp -shikokuchuo.ehime.jp -tobe.ehime.jp -toon.ehime.jp -uchiko.ehime.jp -uwajima.ehime.jp -yawatahama.ehime.jp -echizen.fukui.jp -eiheiji.fukui.jp -fukui.fukui.jp -ikeda.fukui.jp -katsuyama.fukui.jp -mihama.fukui.jp -minamiechizen.fukui.jp -obama.fukui.jp -ohi.fukui.jp -ono.fukui.jp -sabae.fukui.jp -sakai.fukui.jp -takahama.fukui.jp -tsuruga.fukui.jp -wakasa.fukui.jp -ashiya.fukuoka.jp -buzen.fukuoka.jp -chikugo.fukuoka.jp -chikuho.fukuoka.jp -chikujo.fukuoka.jp -chikushino.fukuoka.jp -chikuzen.fukuoka.jp -chuo.fukuoka.jp -dazaifu.fukuoka.jp -fukuchi.fukuoka.jp -hakata.fukuoka.jp -higashi.fukuoka.jp -hirokawa.fukuoka.jp -hisayama.fukuoka.jp -iizuka.fukuoka.jp -inatsuki.fukuoka.jp -kaho.fukuoka.jp -kasuga.fukuoka.jp -kasuya.fukuoka.jp -kawara.fukuoka.jp -keisen.fukuoka.jp -koga.fukuoka.jp -kurate.fukuoka.jp -kurogi.fukuoka.jp -kurume.fukuoka.jp -minami.fukuoka.jp -miyako.fukuoka.jp -miyama.fukuoka.jp -miyawaka.fukuoka.jp -mizumaki.fukuoka.jp -munakata.fukuoka.jp -nakagawa.fukuoka.jp -nakama.fukuoka.jp -nishi.fukuoka.jp -nogata.fukuoka.jp -ogori.fukuoka.jp -okagaki.fukuoka.jp -okawa.fukuoka.jp -oki.fukuoka.jp -omuta.fukuoka.jp -onga.fukuoka.jp -onojo.fukuoka.jp -oto.fukuoka.jp -saigawa.fukuoka.jp -sasaguri.fukuoka.jp -shingu.fukuoka.jp -shinyoshitomi.fukuoka.jp -shonai.fukuoka.jp -soeda.fukuoka.jp -sue.fukuoka.jp -tachiarai.fukuoka.jp -tagawa.fukuoka.jp -takata.fukuoka.jp -toho.fukuoka.jp -toyotsu.fukuoka.jp -tsuiki.fukuoka.jp -ukiha.fukuoka.jp -umi.fukuoka.jp -usui.fukuoka.jp -yamada.fukuoka.jp -yame.fukuoka.jp -yanagawa.fukuoka.jp -yukuhashi.fukuoka.jp -aizubange.fukushima.jp -aizumisato.fukushima.jp -aizuwakamatsu.fukushima.jp -asakawa.fukushima.jp -bandai.fukushima.jp -date.fukushima.jp -fukushima.fukushima.jp -furudono.fukushima.jp -futaba.fukushima.jp -hanawa.fukushima.jp -higashi.fukushima.jp -hirata.fukushima.jp -hirono.fukushima.jp -iitate.fukushima.jp -inawashiro.fukushima.jp -ishikawa.fukushima.jp -iwaki.fukushima.jp -izumizaki.fukushima.jp -kagamiishi.fukushima.jp -kaneyama.fukushima.jp -kawamata.fukushima.jp -kitakata.fukushima.jp -kitashiobara.fukushima.jp -koori.fukushima.jp -koriyama.fukushima.jp -kunimi.fukushima.jp -miharu.fukushima.jp -mishima.fukushima.jp -namie.fukushima.jp -nango.fukushima.jp -nishiaizu.fukushima.jp -nishigo.fukushima.jp -okuma.fukushima.jp -omotego.fukushima.jp -ono.fukushima.jp -otama.fukushima.jp -samegawa.fukushima.jp -shimogo.fukushima.jp -shirakawa.fukushima.jp -showa.fukushima.jp -soma.fukushima.jp -sukagawa.fukushima.jp -taishin.fukushima.jp -tamakawa.fukushima.jp -tanagura.fukushima.jp -tenei.fukushima.jp -yabuki.fukushima.jp -yamato.fukushima.jp -yamatsuri.fukushima.jp -yanaizu.fukushima.jp -yugawa.fukushima.jp -anpachi.gifu.jp -ena.gifu.jp -gifu.gifu.jp -ginan.gifu.jp -godo.gifu.jp -gujo.gifu.jp -hashima.gifu.jp -hichiso.gifu.jp -hida.gifu.jp -higashishirakawa.gifu.jp -ibigawa.gifu.jp -ikeda.gifu.jp -kakamigahara.gifu.jp -kani.gifu.jp -kasahara.gifu.jp -kasamatsu.gifu.jp -kawaue.gifu.jp -kitagata.gifu.jp -mino.gifu.jp -minokamo.gifu.jp -mitake.gifu.jp -mizunami.gifu.jp -motosu.gifu.jp -nakatsugawa.gifu.jp -ogaki.gifu.jp -sakahogi.gifu.jp -seki.gifu.jp -sekigahara.gifu.jp -shirakawa.gifu.jp -tajimi.gifu.jp -takayama.gifu.jp -tarui.gifu.jp -toki.gifu.jp -tomika.gifu.jp -wanouchi.gifu.jp -yamagata.gifu.jp -yaotsu.gifu.jp -yoro.gifu.jp -annaka.gunma.jp -chiyoda.gunma.jp -fujioka.gunma.jp -higashiagatsuma.gunma.jp -isesaki.gunma.jp -itakura.gunma.jp -kanna.gunma.jp -kanra.gunma.jp -katashina.gunma.jp -kawaba.gunma.jp -kiryu.gunma.jp -kusatsu.gunma.jp -maebashi.gunma.jp -meiwa.gunma.jp -midori.gunma.jp -minakami.gunma.jp -naganohara.gunma.jp -nakanojo.gunma.jp -nanmoku.gunma.jp -numata.gunma.jp -oizumi.gunma.jp -ora.gunma.jp -ota.gunma.jp -shibukawa.gunma.jp -shimonita.gunma.jp -shinto.gunma.jp -showa.gunma.jp -takasaki.gunma.jp -takayama.gunma.jp -tamamura.gunma.jp -tatebayashi.gunma.jp -tomioka.gunma.jp -tsukiyono.gunma.jp -tsumagoi.gunma.jp -ueno.gunma.jp -yoshioka.gunma.jp -asaminami.hiroshima.jp -daiwa.hiroshima.jp -etajima.hiroshima.jp -fuchu.hiroshima.jp -fukuyama.hiroshima.jp -hatsukaichi.hiroshima.jp -higashihiroshima.hiroshima.jp -hongo.hiroshima.jp -jinsekikogen.hiroshima.jp -kaita.hiroshima.jp -kui.hiroshima.jp -kumano.hiroshima.jp -kure.hiroshima.jp -mihara.hiroshima.jp -miyoshi.hiroshima.jp -naka.hiroshima.jp -onomichi.hiroshima.jp -osakikamijima.hiroshima.jp -otake.hiroshima.jp -saka.hiroshima.jp -sera.hiroshima.jp -seranishi.hiroshima.jp -shinichi.hiroshima.jp -shobara.hiroshima.jp -takehara.hiroshima.jp -abashiri.hokkaido.jp -abira.hokkaido.jp -aibetsu.hokkaido.jp -akabira.hokkaido.jp -akkeshi.hokkaido.jp -asahikawa.hokkaido.jp -ashibetsu.hokkaido.jp -ashoro.hokkaido.jp -assabu.hokkaido.jp -atsuma.hokkaido.jp -bibai.hokkaido.jp -biei.hokkaido.jp -bifuka.hokkaido.jp -bihoro.hokkaido.jp -biratori.hokkaido.jp -chippubetsu.hokkaido.jp -chitose.hokkaido.jp -date.hokkaido.jp -ebetsu.hokkaido.jp -embetsu.hokkaido.jp -eniwa.hokkaido.jp -erimo.hokkaido.jp -esan.hokkaido.jp -esashi.hokkaido.jp -fukagawa.hokkaido.jp -fukushima.hokkaido.jp -furano.hokkaido.jp -furubira.hokkaido.jp -haboro.hokkaido.jp -hakodate.hokkaido.jp -hamatonbetsu.hokkaido.jp -hidaka.hokkaido.jp -higashikagura.hokkaido.jp -higashikawa.hokkaido.jp -hiroo.hokkaido.jp -hokuryu.hokkaido.jp -hokuto.hokkaido.jp -honbetsu.hokkaido.jp -horokanai.hokkaido.jp -horonobe.hokkaido.jp -ikeda.hokkaido.jp -imakane.hokkaido.jp -ishikari.hokkaido.jp -iwamizawa.hokkaido.jp -iwanai.hokkaido.jp -kamifurano.hokkaido.jp -kamikawa.hokkaido.jp -kamishihoro.hokkaido.jp -kamisunagawa.hokkaido.jp -kamoenai.hokkaido.jp -kayabe.hokkaido.jp -kembuchi.hokkaido.jp -kikonai.hokkaido.jp -kimobetsu.hokkaido.jp -kitahiroshima.hokkaido.jp -kitami.hokkaido.jp -kiyosato.hokkaido.jp -koshimizu.hokkaido.jp -kunneppu.hokkaido.jp -kuriyama.hokkaido.jp -kuromatsunai.hokkaido.jp -kushiro.hokkaido.jp -kutchan.hokkaido.jp -kyowa.hokkaido.jp -mashike.hokkaido.jp -matsumae.hokkaido.jp -mikasa.hokkaido.jp -minamifurano.hokkaido.jp -mombetsu.hokkaido.jp -moseushi.hokkaido.jp -mukawa.hokkaido.jp -muroran.hokkaido.jp -naie.hokkaido.jp -nakagawa.hokkaido.jp -nakasatsunai.hokkaido.jp -nakatombetsu.hokkaido.jp -nanae.hokkaido.jp -nanporo.hokkaido.jp -nayoro.hokkaido.jp -nemuro.hokkaido.jp -niikappu.hokkaido.jp -niki.hokkaido.jp -nishiokoppe.hokkaido.jp -noboribetsu.hokkaido.jp -numata.hokkaido.jp -obihiro.hokkaido.jp -obira.hokkaido.jp -oketo.hokkaido.jp -okoppe.hokkaido.jp -otaru.hokkaido.jp -otobe.hokkaido.jp -otofuke.hokkaido.jp -otoineppu.hokkaido.jp -oumu.hokkaido.jp -ozora.hokkaido.jp -pippu.hokkaido.jp -rankoshi.hokkaido.jp -rebun.hokkaido.jp -rikubetsu.hokkaido.jp -rishiri.hokkaido.jp -rishirifuji.hokkaido.jp -saroma.hokkaido.jp -sarufutsu.hokkaido.jp -shakotan.hokkaido.jp -shari.hokkaido.jp -shibecha.hokkaido.jp -shibetsu.hokkaido.jp -shikabe.hokkaido.jp -shikaoi.hokkaido.jp -shimamaki.hokkaido.jp -shimizu.hokkaido.jp -shimokawa.hokkaido.jp -shinshinotsu.hokkaido.jp -shintoku.hokkaido.jp -shiranuka.hokkaido.jp -shiraoi.hokkaido.jp -shiriuchi.hokkaido.jp -sobetsu.hokkaido.jp -sunagawa.hokkaido.jp -taiki.hokkaido.jp -takasu.hokkaido.jp -takikawa.hokkaido.jp -takinoue.hokkaido.jp -teshikaga.hokkaido.jp -tobetsu.hokkaido.jp -tohma.hokkaido.jp -tomakomai.hokkaido.jp -tomari.hokkaido.jp -toya.hokkaido.jp -toyako.hokkaido.jp -toyotomi.hokkaido.jp -toyoura.hokkaido.jp -tsubetsu.hokkaido.jp -tsukigata.hokkaido.jp -urakawa.hokkaido.jp -urausu.hokkaido.jp -uryu.hokkaido.jp -utashinai.hokkaido.jp -wakkanai.hokkaido.jp -wassamu.hokkaido.jp -yakumo.hokkaido.jp -yoichi.hokkaido.jp -aioi.hyogo.jp -akashi.hyogo.jp -ako.hyogo.jp -amagasaki.hyogo.jp -aogaki.hyogo.jp -asago.hyogo.jp -ashiya.hyogo.jp -awaji.hyogo.jp -fukusaki.hyogo.jp -goshiki.hyogo.jp -harima.hyogo.jp -himeji.hyogo.jp -ichikawa.hyogo.jp -inagawa.hyogo.jp -itami.hyogo.jp -kakogawa.hyogo.jp -kamigori.hyogo.jp -kamikawa.hyogo.jp -kasai.hyogo.jp -kasuga.hyogo.jp -kawanishi.hyogo.jp -miki.hyogo.jp -minamiawaji.hyogo.jp -nishinomiya.hyogo.jp -nishiwaki.hyogo.jp -ono.hyogo.jp -sanda.hyogo.jp -sannan.hyogo.jp -sasayama.hyogo.jp -sayo.hyogo.jp -shingu.hyogo.jp -shinonsen.hyogo.jp -shiso.hyogo.jp -sumoto.hyogo.jp -taishi.hyogo.jp -taka.hyogo.jp -takarazuka.hyogo.jp -takasago.hyogo.jp -takino.hyogo.jp -tamba.hyogo.jp -tatsuno.hyogo.jp -toyooka.hyogo.jp -yabu.hyogo.jp -yashiro.hyogo.jp -yoka.hyogo.jp -yokawa.hyogo.jp -ami.ibaraki.jp -asahi.ibaraki.jp -bando.ibaraki.jp -chikusei.ibaraki.jp -daigo.ibaraki.jp -fujishiro.ibaraki.jp -hitachi.ibaraki.jp -hitachinaka.ibaraki.jp -hitachiomiya.ibaraki.jp -hitachiota.ibaraki.jp -ibaraki.ibaraki.jp -ina.ibaraki.jp -inashiki.ibaraki.jp -itako.ibaraki.jp -iwama.ibaraki.jp -joso.ibaraki.jp -kamisu.ibaraki.jp -kasama.ibaraki.jp -kashima.ibaraki.jp -kasumigaura.ibaraki.jp -koga.ibaraki.jp -miho.ibaraki.jp -mito.ibaraki.jp -moriya.ibaraki.jp -naka.ibaraki.jp -namegata.ibaraki.jp -oarai.ibaraki.jp -ogawa.ibaraki.jp -omitama.ibaraki.jp -ryugasaki.ibaraki.jp -sakai.ibaraki.jp -sakuragawa.ibaraki.jp -shimodate.ibaraki.jp -shimotsuma.ibaraki.jp -shirosato.ibaraki.jp -sowa.ibaraki.jp -suifu.ibaraki.jp -takahagi.ibaraki.jp -tamatsukuri.ibaraki.jp -tokai.ibaraki.jp -tomobe.ibaraki.jp -tone.ibaraki.jp -toride.ibaraki.jp -tsuchiura.ibaraki.jp -tsukuba.ibaraki.jp -uchihara.ibaraki.jp -ushiku.ibaraki.jp -yachiyo.ibaraki.jp -yamagata.ibaraki.jp -yawara.ibaraki.jp -yuki.ibaraki.jp -anamizu.ishikawa.jp -hakui.ishikawa.jp -hakusan.ishikawa.jp -kaga.ishikawa.jp -kahoku.ishikawa.jp -kanazawa.ishikawa.jp -kawakita.ishikawa.jp -komatsu.ishikawa.jp -nakanoto.ishikawa.jp -nanao.ishikawa.jp -nomi.ishikawa.jp -nonoichi.ishikawa.jp -noto.ishikawa.jp -shika.ishikawa.jp -suzu.ishikawa.jp -tsubata.ishikawa.jp -tsurugi.ishikawa.jp -uchinada.ishikawa.jp -wajima.ishikawa.jp -fudai.iwate.jp -fujisawa.iwate.jp -hanamaki.iwate.jp -hiraizumi.iwate.jp -hirono.iwate.jp -ichinohe.iwate.jp -ichinoseki.iwate.jp -iwaizumi.iwate.jp -iwate.iwate.jp -joboji.iwate.jp -kamaishi.iwate.jp -kanegasaki.iwate.jp -karumai.iwate.jp -kawai.iwate.jp -kitakami.iwate.jp -kuji.iwate.jp -kunohe.iwate.jp -kuzumaki.iwate.jp -miyako.iwate.jp -mizusawa.iwate.jp -morioka.iwate.jp -ninohe.iwate.jp -noda.iwate.jp -ofunato.iwate.jp -oshu.iwate.jp -otsuchi.iwate.jp -rikuzentakata.iwate.jp -shiwa.iwate.jp -shizukuishi.iwate.jp -sumita.iwate.jp -tanohata.iwate.jp -tono.iwate.jp -yahaba.iwate.jp -yamada.iwate.jp -ayagawa.kagawa.jp -higashikagawa.kagawa.jp -kanonji.kagawa.jp -kotohira.kagawa.jp -manno.kagawa.jp -marugame.kagawa.jp -mitoyo.kagawa.jp -naoshima.kagawa.jp -sanuki.kagawa.jp -tadotsu.kagawa.jp -takamatsu.kagawa.jp -tonosho.kagawa.jp -uchinomi.kagawa.jp -utazu.kagawa.jp -zentsuji.kagawa.jp -akune.kagoshima.jp -amami.kagoshima.jp -hioki.kagoshima.jp -isa.kagoshima.jp -isen.kagoshima.jp -izumi.kagoshima.jp -kagoshima.kagoshima.jp -kanoya.kagoshima.jp -kawanabe.kagoshima.jp -kinko.kagoshima.jp -kouyama.kagoshima.jp -makurazaki.kagoshima.jp -matsumoto.kagoshima.jp -minamitane.kagoshima.jp -nakatane.kagoshima.jp -nishinoomote.kagoshima.jp -satsumasendai.kagoshima.jp -soo.kagoshima.jp -tarumizu.kagoshima.jp -yusui.kagoshima.jp -aikawa.kanagawa.jp -atsugi.kanagawa.jp -ayase.kanagawa.jp -chigasaki.kanagawa.jp -ebina.kanagawa.jp -fujisawa.kanagawa.jp -hadano.kanagawa.jp -hakone.kanagawa.jp -hiratsuka.kanagawa.jp -isehara.kanagawa.jp -kaisei.kanagawa.jp -kamakura.kanagawa.jp -kiyokawa.kanagawa.jp -matsuda.kanagawa.jp -minamiashigara.kanagawa.jp -miura.kanagawa.jp -nakai.kanagawa.jp -ninomiya.kanagawa.jp -odawara.kanagawa.jp -oi.kanagawa.jp -oiso.kanagawa.jp -sagamihara.kanagawa.jp -samukawa.kanagawa.jp -tsukui.kanagawa.jp -yamakita.kanagawa.jp -yamato.kanagawa.jp -yokosuka.kanagawa.jp -yugawara.kanagawa.jp -zama.kanagawa.jp -zushi.kanagawa.jp -aki.kochi.jp -geisei.kochi.jp -hidaka.kochi.jp -higashitsuno.kochi.jp -ino.kochi.jp -kagami.kochi.jp -kami.kochi.jp -kitagawa.kochi.jp -kochi.kochi.jp -mihara.kochi.jp -motoyama.kochi.jp -muroto.kochi.jp -nahari.kochi.jp -nakamura.kochi.jp -nankoku.kochi.jp -nishitosa.kochi.jp -niyodogawa.kochi.jp -ochi.kochi.jp -okawa.kochi.jp -otoyo.kochi.jp -otsuki.kochi.jp -sakawa.kochi.jp -sukumo.kochi.jp -susaki.kochi.jp -tosa.kochi.jp -tosashimizu.kochi.jp -toyo.kochi.jp -tsuno.kochi.jp -umaji.kochi.jp -yasuda.kochi.jp -yusuhara.kochi.jp -amakusa.kumamoto.jp -arao.kumamoto.jp -aso.kumamoto.jp -choyo.kumamoto.jp -gyokuto.kumamoto.jp -kamiamakusa.kumamoto.jp -kikuchi.kumamoto.jp -kumamoto.kumamoto.jp -mashiki.kumamoto.jp -mifune.kumamoto.jp -minamata.kumamoto.jp -minamioguni.kumamoto.jp -nagasu.kumamoto.jp -nishihara.kumamoto.jp -oguni.kumamoto.jp -ozu.kumamoto.jp -sumoto.kumamoto.jp -takamori.kumamoto.jp -uki.kumamoto.jp -uto.kumamoto.jp -yamaga.kumamoto.jp -yamato.kumamoto.jp -yatsushiro.kumamoto.jp -ayabe.kyoto.jp -fukuchiyama.kyoto.jp -higashiyama.kyoto.jp -ide.kyoto.jp -ine.kyoto.jp -joyo.kyoto.jp -kameoka.kyoto.jp -kamo.kyoto.jp -kita.kyoto.jp -kizu.kyoto.jp -kumiyama.kyoto.jp -kyotamba.kyoto.jp -kyotanabe.kyoto.jp -kyotango.kyoto.jp -maizuru.kyoto.jp -minami.kyoto.jp -minamiyamashiro.kyoto.jp -miyazu.kyoto.jp -muko.kyoto.jp -nagaokakyo.kyoto.jp -nakagyo.kyoto.jp -nantan.kyoto.jp -oyamazaki.kyoto.jp -sakyo.kyoto.jp -seika.kyoto.jp -tanabe.kyoto.jp -uji.kyoto.jp -ujitawara.kyoto.jp -wazuka.kyoto.jp -yamashina.kyoto.jp -yawata.kyoto.jp -asahi.mie.jp -inabe.mie.jp -ise.mie.jp -kameyama.mie.jp -kawagoe.mie.jp -kiho.mie.jp -kisosaki.mie.jp -kiwa.mie.jp -komono.mie.jp -kumano.mie.jp -kuwana.mie.jp -matsusaka.mie.jp -meiwa.mie.jp -mihama.mie.jp -minamiise.mie.jp -misugi.mie.jp -miyama.mie.jp -nabari.mie.jp -shima.mie.jp -suzuka.mie.jp -tado.mie.jp -taiki.mie.jp -taki.mie.jp -tamaki.mie.jp -toba.mie.jp -tsu.mie.jp -udono.mie.jp -ureshino.mie.jp -watarai.mie.jp -yokkaichi.mie.jp -furukawa.miyagi.jp -higashimatsushima.miyagi.jp -ishinomaki.miyagi.jp -iwanuma.miyagi.jp -kakuda.miyagi.jp -kami.miyagi.jp -kawasaki.miyagi.jp -marumori.miyagi.jp -matsushima.miyagi.jp -minamisanriku.miyagi.jp -misato.miyagi.jp -murata.miyagi.jp -natori.miyagi.jp -ogawara.miyagi.jp -ohira.miyagi.jp -onagawa.miyagi.jp -osaki.miyagi.jp -rifu.miyagi.jp -semine.miyagi.jp -shibata.miyagi.jp -shichikashuku.miyagi.jp -shikama.miyagi.jp -shiogama.miyagi.jp -shiroishi.miyagi.jp -tagajo.miyagi.jp -taiwa.miyagi.jp -tome.miyagi.jp -tomiya.miyagi.jp -wakuya.miyagi.jp -watari.miyagi.jp -yamamoto.miyagi.jp -zao.miyagi.jp -aya.miyazaki.jp -ebino.miyazaki.jp -gokase.miyazaki.jp -hyuga.miyazaki.jp -kadogawa.miyazaki.jp -kawaminami.miyazaki.jp -kijo.miyazaki.jp -kitagawa.miyazaki.jp -kitakata.miyazaki.jp -kitaura.miyazaki.jp -kobayashi.miyazaki.jp -kunitomi.miyazaki.jp -kushima.miyazaki.jp -mimata.miyazaki.jp -miyakonojo.miyazaki.jp -miyazaki.miyazaki.jp -morotsuka.miyazaki.jp -nichinan.miyazaki.jp -nishimera.miyazaki.jp -nobeoka.miyazaki.jp -saito.miyazaki.jp -shiiba.miyazaki.jp -shintomi.miyazaki.jp -takaharu.miyazaki.jp -takanabe.miyazaki.jp -takazaki.miyazaki.jp -tsuno.miyazaki.jp -achi.nagano.jp -agematsu.nagano.jp -anan.nagano.jp -aoki.nagano.jp -asahi.nagano.jp -azumino.nagano.jp -chikuhoku.nagano.jp -chikuma.nagano.jp -chino.nagano.jp -fujimi.nagano.jp -hakuba.nagano.jp -hara.nagano.jp -hiraya.nagano.jp -iida.nagano.jp -iijima.nagano.jp -iiyama.nagano.jp -iizuna.nagano.jp -ikeda.nagano.jp -ikusaka.nagano.jp -ina.nagano.jp -karuizawa.nagano.jp -kawakami.nagano.jp -kiso.nagano.jp -kisofukushima.nagano.jp -kitaaiki.nagano.jp -komagane.nagano.jp -komoro.nagano.jp -matsukawa.nagano.jp -matsumoto.nagano.jp -miasa.nagano.jp -minamiaiki.nagano.jp -minamimaki.nagano.jp -minamiminowa.nagano.jp -minowa.nagano.jp -miyada.nagano.jp -miyota.nagano.jp -mochizuki.nagano.jp -nagano.nagano.jp -nagawa.nagano.jp -nagiso.nagano.jp -nakagawa.nagano.jp -nakano.nagano.jp -nozawaonsen.nagano.jp -obuse.nagano.jp -ogawa.nagano.jp -okaya.nagano.jp -omachi.nagano.jp -omi.nagano.jp -ookuwa.nagano.jp -ooshika.nagano.jp -otaki.nagano.jp -otari.nagano.jp -sakae.nagano.jp -sakaki.nagano.jp -saku.nagano.jp -sakuho.nagano.jp -shimosuwa.nagano.jp -shinanomachi.nagano.jp -shiojiri.nagano.jp -suwa.nagano.jp -suzaka.nagano.jp -takagi.nagano.jp -takamori.nagano.jp -takayama.nagano.jp -tateshina.nagano.jp -tatsuno.nagano.jp -togakushi.nagano.jp -togura.nagano.jp -tomi.nagano.jp -ueda.nagano.jp -wada.nagano.jp -yamagata.nagano.jp -yamanouchi.nagano.jp -yasaka.nagano.jp -yasuoka.nagano.jp -chijiwa.nagasaki.jp -futsu.nagasaki.jp -goto.nagasaki.jp -hasami.nagasaki.jp -hirado.nagasaki.jp -iki.nagasaki.jp -isahaya.nagasaki.jp -kawatana.nagasaki.jp -kuchinotsu.nagasaki.jp -matsuura.nagasaki.jp -nagasaki.nagasaki.jp -obama.nagasaki.jp -omura.nagasaki.jp -oseto.nagasaki.jp -saikai.nagasaki.jp -sasebo.nagasaki.jp -seihi.nagasaki.jp -shimabara.nagasaki.jp -shinkamigoto.nagasaki.jp -togitsu.nagasaki.jp -tsushima.nagasaki.jp -unzen.nagasaki.jp -ando.nara.jp -gose.nara.jp -heguri.nara.jp -higashiyoshino.nara.jp -ikaruga.nara.jp -ikoma.nara.jp -kamikitayama.nara.jp -kanmaki.nara.jp -kashiba.nara.jp -kashihara.nara.jp -katsuragi.nara.jp -kawai.nara.jp -kawakami.nara.jp -kawanishi.nara.jp -koryo.nara.jp -kurotaki.nara.jp -mitsue.nara.jp -miyake.nara.jp -nara.nara.jp -nosegawa.nara.jp -oji.nara.jp -ouda.nara.jp -oyodo.nara.jp -sakurai.nara.jp -sango.nara.jp -shimoichi.nara.jp -shimokitayama.nara.jp -shinjo.nara.jp -soni.nara.jp -takatori.nara.jp -tawaramoto.nara.jp -tenkawa.nara.jp -tenri.nara.jp -uda.nara.jp -yamatokoriyama.nara.jp -yamatotakada.nara.jp -yamazoe.nara.jp -yoshino.nara.jp -aga.niigata.jp -agano.niigata.jp -gosen.niigata.jp -itoigawa.niigata.jp -izumozaki.niigata.jp -joetsu.niigata.jp -kamo.niigata.jp -kariwa.niigata.jp -kashiwazaki.niigata.jp -minamiuonuma.niigata.jp -mitsuke.niigata.jp -muika.niigata.jp -murakami.niigata.jp -myoko.niigata.jp -nagaoka.niigata.jp -niigata.niigata.jp -ojiya.niigata.jp -omi.niigata.jp -sado.niigata.jp -sanjo.niigata.jp -seiro.niigata.jp -seirou.niigata.jp -sekikawa.niigata.jp -shibata.niigata.jp -tagami.niigata.jp -tainai.niigata.jp -tochio.niigata.jp -tokamachi.niigata.jp -tsubame.niigata.jp -tsunan.niigata.jp -uonuma.niigata.jp -yahiko.niigata.jp -yoita.niigata.jp -yuzawa.niigata.jp -beppu.oita.jp -bungoono.oita.jp -bungotakada.oita.jp -hasama.oita.jp -hiji.oita.jp -himeshima.oita.jp -hita.oita.jp -kamitsue.oita.jp -kokonoe.oita.jp -kuju.oita.jp -kunisaki.oita.jp -kusu.oita.jp -oita.oita.jp -saiki.oita.jp -taketa.oita.jp -tsukumi.oita.jp -usa.oita.jp -usuki.oita.jp -yufu.oita.jp -akaiwa.okayama.jp -asakuchi.okayama.jp -bizen.okayama.jp -hayashima.okayama.jp -ibara.okayama.jp -kagamino.okayama.jp -kasaoka.okayama.jp -kibichuo.okayama.jp -kumenan.okayama.jp -kurashiki.okayama.jp -maniwa.okayama.jp -misaki.okayama.jp -nagi.okayama.jp -niimi.okayama.jp -nishiawakura.okayama.jp -okayama.okayama.jp -satosho.okayama.jp -setouchi.okayama.jp -shinjo.okayama.jp -shoo.okayama.jp -soja.okayama.jp -takahashi.okayama.jp -tamano.okayama.jp -tsuyama.okayama.jp -wake.okayama.jp -yakage.okayama.jp -aguni.okinawa.jp -ginowan.okinawa.jp -ginoza.okinawa.jp -gushikami.okinawa.jp -haebaru.okinawa.jp -higashi.okinawa.jp -hirara.okinawa.jp -iheya.okinawa.jp -ishigaki.okinawa.jp -ishikawa.okinawa.jp -itoman.okinawa.jp -izena.okinawa.jp -kadena.okinawa.jp -kin.okinawa.jp -kitadaito.okinawa.jp -kitanakagusuku.okinawa.jp -kumejima.okinawa.jp -kunigami.okinawa.jp -minamidaito.okinawa.jp -motobu.okinawa.jp -nago.okinawa.jp -naha.okinawa.jp -nakagusuku.okinawa.jp -nakijin.okinawa.jp -nanjo.okinawa.jp -nishihara.okinawa.jp -ogimi.okinawa.jp -okinawa.okinawa.jp -onna.okinawa.jp -shimoji.okinawa.jp -taketomi.okinawa.jp -tarama.okinawa.jp -tokashiki.okinawa.jp -tomigusuku.okinawa.jp -tonaki.okinawa.jp -urasoe.okinawa.jp -uruma.okinawa.jp -yaese.okinawa.jp -yomitan.okinawa.jp -yonabaru.okinawa.jp -yonaguni.okinawa.jp -zamami.okinawa.jp -abeno.osaka.jp -chihayaakasaka.osaka.jp -chuo.osaka.jp -daito.osaka.jp -fujiidera.osaka.jp -habikino.osaka.jp -hannan.osaka.jp -higashiosaka.osaka.jp -higashisumiyoshi.osaka.jp -higashiyodogawa.osaka.jp -hirakata.osaka.jp -ibaraki.osaka.jp -ikeda.osaka.jp -izumi.osaka.jp -izumiotsu.osaka.jp -izumisano.osaka.jp -kadoma.osaka.jp -kaizuka.osaka.jp -kanan.osaka.jp -kashiwara.osaka.jp -katano.osaka.jp -kawachinagano.osaka.jp -kishiwada.osaka.jp -kita.osaka.jp -kumatori.osaka.jp -matsubara.osaka.jp -minato.osaka.jp -minoh.osaka.jp -misaki.osaka.jp -moriguchi.osaka.jp -neyagawa.osaka.jp -nishi.osaka.jp -nose.osaka.jp -osakasayama.osaka.jp -sakai.osaka.jp -sayama.osaka.jp -sennan.osaka.jp -settsu.osaka.jp -shijonawate.osaka.jp -shimamoto.osaka.jp -suita.osaka.jp -tadaoka.osaka.jp -taishi.osaka.jp -tajiri.osaka.jp -takaishi.osaka.jp -takatsuki.osaka.jp -tondabayashi.osaka.jp -toyonaka.osaka.jp -toyono.osaka.jp -yao.osaka.jp -ariake.saga.jp -arita.saga.jp -fukudomi.saga.jp -genkai.saga.jp -hamatama.saga.jp -hizen.saga.jp -imari.saga.jp -kamimine.saga.jp -kanzaki.saga.jp -karatsu.saga.jp -kashima.saga.jp -kitagata.saga.jp -kitahata.saga.jp -kiyama.saga.jp -kouhoku.saga.jp -kyuragi.saga.jp -nishiarita.saga.jp -ogi.saga.jp -omachi.saga.jp -ouchi.saga.jp -saga.saga.jp -shiroishi.saga.jp -taku.saga.jp -tara.saga.jp -tosu.saga.jp -yoshinogari.saga.jp -arakawa.saitama.jp -asaka.saitama.jp -chichibu.saitama.jp -fujimi.saitama.jp -fujimino.saitama.jp -fukaya.saitama.jp -hanno.saitama.jp -hanyu.saitama.jp -hasuda.saitama.jp -hatogaya.saitama.jp -hatoyama.saitama.jp -hidaka.saitama.jp -higashichichibu.saitama.jp -higashimatsuyama.saitama.jp -honjo.saitama.jp -ina.saitama.jp -iruma.saitama.jp -iwatsuki.saitama.jp -kamiizumi.saitama.jp -kamikawa.saitama.jp -kamisato.saitama.jp -kasukabe.saitama.jp -kawagoe.saitama.jp -kawaguchi.saitama.jp -kawajima.saitama.jp -kazo.saitama.jp -kitamoto.saitama.jp -koshigaya.saitama.jp -kounosu.saitama.jp -kuki.saitama.jp -kumagaya.saitama.jp -matsubushi.saitama.jp -minano.saitama.jp -misato.saitama.jp -miyashiro.saitama.jp -miyoshi.saitama.jp -moroyama.saitama.jp -nagatoro.saitama.jp -namegawa.saitama.jp -niiza.saitama.jp -ogano.saitama.jp -ogawa.saitama.jp -ogose.saitama.jp -okegawa.saitama.jp -omiya.saitama.jp -otaki.saitama.jp -ranzan.saitama.jp -ryokami.saitama.jp -saitama.saitama.jp -sakado.saitama.jp -satte.saitama.jp -sayama.saitama.jp -shiki.saitama.jp -shiraoka.saitama.jp -soka.saitama.jp -sugito.saitama.jp -toda.saitama.jp -tokigawa.saitama.jp -tokorozawa.saitama.jp -tsurugashima.saitama.jp -urawa.saitama.jp -warabi.saitama.jp -yashio.saitama.jp -yokoze.saitama.jp -yono.saitama.jp -yorii.saitama.jp -yoshida.saitama.jp -yoshikawa.saitama.jp -yoshimi.saitama.jp -aisho.shiga.jp -gamo.shiga.jp -higashiomi.shiga.jp -hikone.shiga.jp -koka.shiga.jp -konan.shiga.jp -kosei.shiga.jp -koto.shiga.jp -kusatsu.shiga.jp -maibara.shiga.jp -moriyama.shiga.jp -nagahama.shiga.jp -nishiazai.shiga.jp -notogawa.shiga.jp -omihachiman.shiga.jp -otsu.shiga.jp -ritto.shiga.jp -ryuoh.shiga.jp -takashima.shiga.jp -takatsuki.shiga.jp -torahime.shiga.jp -toyosato.shiga.jp -yasu.shiga.jp -akagi.shimane.jp -ama.shimane.jp -gotsu.shimane.jp -hamada.shimane.jp -higashiizumo.shimane.jp -hikawa.shimane.jp -hikimi.shimane.jp -izumo.shimane.jp -kakinoki.shimane.jp -masuda.shimane.jp -matsue.shimane.jp -misato.shimane.jp -nishinoshima.shimane.jp -ohda.shimane.jp -okinoshima.shimane.jp -okuizumo.shimane.jp -shimane.shimane.jp -tamayu.shimane.jp -tsuwano.shimane.jp -unnan.shimane.jp -yakumo.shimane.jp -yasugi.shimane.jp -yatsuka.shimane.jp -arai.shizuoka.jp -atami.shizuoka.jp -fuji.shizuoka.jp -fujieda.shizuoka.jp -fujikawa.shizuoka.jp -fujinomiya.shizuoka.jp -fukuroi.shizuoka.jp -gotemba.shizuoka.jp -haibara.shizuoka.jp -hamamatsu.shizuoka.jp -higashiizu.shizuoka.jp -ito.shizuoka.jp -iwata.shizuoka.jp -izu.shizuoka.jp -izunokuni.shizuoka.jp -kakegawa.shizuoka.jp -kannami.shizuoka.jp -kawanehon.shizuoka.jp -kawazu.shizuoka.jp -kikugawa.shizuoka.jp -kosai.shizuoka.jp -makinohara.shizuoka.jp -matsuzaki.shizuoka.jp -minamiizu.shizuoka.jp -mishima.shizuoka.jp -morimachi.shizuoka.jp -nishiizu.shizuoka.jp -numazu.shizuoka.jp -omaezaki.shizuoka.jp -shimada.shizuoka.jp -shimizu.shizuoka.jp -shimoda.shizuoka.jp -shizuoka.shizuoka.jp -susono.shizuoka.jp -yaizu.shizuoka.jp -yoshida.shizuoka.jp -ashikaga.tochigi.jp -bato.tochigi.jp -haga.tochigi.jp -ichikai.tochigi.jp -iwafune.tochigi.jp -kaminokawa.tochigi.jp -kanuma.tochigi.jp -karasuyama.tochigi.jp -kuroiso.tochigi.jp -mashiko.tochigi.jp -mibu.tochigi.jp -moka.tochigi.jp -motegi.tochigi.jp -nasu.tochigi.jp -nasushiobara.tochigi.jp -nikko.tochigi.jp -nishikata.tochigi.jp -nogi.tochigi.jp -ohira.tochigi.jp -ohtawara.tochigi.jp -oyama.tochigi.jp -sakura.tochigi.jp -sano.tochigi.jp -shimotsuke.tochigi.jp -shioya.tochigi.jp -takanezawa.tochigi.jp -tochigi.tochigi.jp -tsuga.tochigi.jp -ujiie.tochigi.jp -utsunomiya.tochigi.jp -yaita.tochigi.jp -aizumi.tokushima.jp -anan.tokushima.jp -ichiba.tokushima.jp -itano.tokushima.jp -kainan.tokushima.jp -komatsushima.tokushima.jp -matsushige.tokushima.jp -mima.tokushima.jp -minami.tokushima.jp -miyoshi.tokushima.jp -mugi.tokushima.jp -nakagawa.tokushima.jp -naruto.tokushima.jp -sanagochi.tokushima.jp -shishikui.tokushima.jp -tokushima.tokushima.jp -wajiki.tokushima.jp -adachi.tokyo.jp -akiruno.tokyo.jp -akishima.tokyo.jp -aogashima.tokyo.jp -arakawa.tokyo.jp -bunkyo.tokyo.jp -chiyoda.tokyo.jp -chofu.tokyo.jp -chuo.tokyo.jp -edogawa.tokyo.jp -fuchu.tokyo.jp -fussa.tokyo.jp -hachijo.tokyo.jp -hachioji.tokyo.jp -hamura.tokyo.jp -higashikurume.tokyo.jp -higashimurayama.tokyo.jp -higashiyamato.tokyo.jp -hino.tokyo.jp -hinode.tokyo.jp -hinohara.tokyo.jp -inagi.tokyo.jp -itabashi.tokyo.jp -katsushika.tokyo.jp -kita.tokyo.jp -kiyose.tokyo.jp -kodaira.tokyo.jp -koganei.tokyo.jp -kokubunji.tokyo.jp -komae.tokyo.jp -koto.tokyo.jp -kouzushima.tokyo.jp -kunitachi.tokyo.jp -machida.tokyo.jp -meguro.tokyo.jp -minato.tokyo.jp -mitaka.tokyo.jp -mizuho.tokyo.jp -musashimurayama.tokyo.jp -musashino.tokyo.jp -nakano.tokyo.jp -nerima.tokyo.jp -ogasawara.tokyo.jp -okutama.tokyo.jp -ome.tokyo.jp -oshima.tokyo.jp -ota.tokyo.jp -setagaya.tokyo.jp -shibuya.tokyo.jp -shinagawa.tokyo.jp -shinjuku.tokyo.jp -suginami.tokyo.jp -sumida.tokyo.jp -tachikawa.tokyo.jp -taito.tokyo.jp -tama.tokyo.jp -toshima.tokyo.jp -chizu.tottori.jp -hino.tottori.jp -kawahara.tottori.jp -koge.tottori.jp -kotoura.tottori.jp -misasa.tottori.jp -nanbu.tottori.jp -nichinan.tottori.jp -sakaiminato.tottori.jp -tottori.tottori.jp -wakasa.tottori.jp -yazu.tottori.jp -yonago.tottori.jp -asahi.toyama.jp -fuchu.toyama.jp -fukumitsu.toyama.jp -funahashi.toyama.jp -himi.toyama.jp -imizu.toyama.jp -inami.toyama.jp -johana.toyama.jp -kamiichi.toyama.jp -kurobe.toyama.jp -nakaniikawa.toyama.jp -namerikawa.toyama.jp -nanto.toyama.jp -nyuzen.toyama.jp -oyabe.toyama.jp -taira.toyama.jp -takaoka.toyama.jp -tateyama.toyama.jp -toga.toyama.jp -tonami.toyama.jp -toyama.toyama.jp -unazuki.toyama.jp -uozu.toyama.jp -yamada.toyama.jp -arida.wakayama.jp -aridagawa.wakayama.jp -gobo.wakayama.jp -hashimoto.wakayama.jp -hidaka.wakayama.jp -hirogawa.wakayama.jp -inami.wakayama.jp -iwade.wakayama.jp -kainan.wakayama.jp -kamitonda.wakayama.jp -katsuragi.wakayama.jp -kimino.wakayama.jp -kinokawa.wakayama.jp -kitayama.wakayama.jp -koya.wakayama.jp -koza.wakayama.jp -kozagawa.wakayama.jp -kudoyama.wakayama.jp -kushimoto.wakayama.jp -mihama.wakayama.jp -misato.wakayama.jp -nachikatsuura.wakayama.jp -shingu.wakayama.jp -shirahama.wakayama.jp -taiji.wakayama.jp -tanabe.wakayama.jp -wakayama.wakayama.jp -yuasa.wakayama.jp -yura.wakayama.jp -asahi.yamagata.jp -funagata.yamagata.jp -higashine.yamagata.jp -iide.yamagata.jp -kahoku.yamagata.jp -kaminoyama.yamagata.jp -kaneyama.yamagata.jp -kawanishi.yamagata.jp -mamurogawa.yamagata.jp -mikawa.yamagata.jp -murayama.yamagata.jp -nagai.yamagata.jp -nakayama.yamagata.jp -nanyo.yamagata.jp -nishikawa.yamagata.jp -obanazawa.yamagata.jp -oe.yamagata.jp -oguni.yamagata.jp -ohkura.yamagata.jp -oishida.yamagata.jp -sagae.yamagata.jp -sakata.yamagata.jp -sakegawa.yamagata.jp -shinjo.yamagata.jp -shirataka.yamagata.jp -shonai.yamagata.jp -takahata.yamagata.jp -tendo.yamagata.jp -tozawa.yamagata.jp -tsuruoka.yamagata.jp -yamagata.yamagata.jp -yamanobe.yamagata.jp -yonezawa.yamagata.jp -yuza.yamagata.jp -abu.yamaguchi.jp -hagi.yamaguchi.jp -hikari.yamaguchi.jp -hofu.yamaguchi.jp -iwakuni.yamaguchi.jp -kudamatsu.yamaguchi.jp -mitou.yamaguchi.jp -nagato.yamaguchi.jp -oshima.yamaguchi.jp -shimonoseki.yamaguchi.jp -shunan.yamaguchi.jp -tabuse.yamaguchi.jp -tokuyama.yamaguchi.jp -toyota.yamaguchi.jp -ube.yamaguchi.jp -yuu.yamaguchi.jp -chuo.yamanashi.jp -doshi.yamanashi.jp -fuefuki.yamanashi.jp -fujikawa.yamanashi.jp -fujikawaguchiko.yamanashi.jp -fujiyoshida.yamanashi.jp -hayakawa.yamanashi.jp -hokuto.yamanashi.jp -ichikawamisato.yamanashi.jp -kai.yamanashi.jp -kofu.yamanashi.jp -koshu.yamanashi.jp -kosuge.yamanashi.jp -minami-alps.yamanashi.jp -minobu.yamanashi.jp -nakamichi.yamanashi.jp -nanbu.yamanashi.jp -narusawa.yamanashi.jp -nirasaki.yamanashi.jp -nishikatsura.yamanashi.jp -oshino.yamanashi.jp -otsuki.yamanashi.jp -showa.yamanashi.jp -tabayama.yamanashi.jp -tsuru.yamanashi.jp -uenohara.yamanashi.jp -yamanakako.yamanashi.jp -yamanashi.yamanashi.jp - -// ke : http://www.kenic.or.ke/index.php/en/ke-domains/ke-domains -ke -ac.ke -co.ke -go.ke -info.ke -me.ke -mobi.ke -ne.ke -or.ke -sc.ke - -// kg : http://www.domain.kg/dmn_n.html -kg -org.kg -net.kg -com.kg -edu.kg -gov.kg -mil.kg - -// kh : http://www.mptc.gov.kh/dns_registration.htm -*.kh - -// ki : http://www.ki/dns/index.html -ki -edu.ki -biz.ki -net.ki -org.ki -gov.ki -info.ki -com.ki - -// km : https://en.wikipedia.org/wiki/.km -// http://www.domaine.km/documents/charte.doc -km -org.km -nom.km -gov.km -prd.km -tm.km -edu.km -mil.km -ass.km -com.km -// These are only mentioned as proposed suggestions at domaine.km, but -// https://en.wikipedia.org/wiki/.km says they're available for registration: -coop.km -asso.km -presse.km -medecin.km -notaires.km -pharmaciens.km -veterinaire.km -gouv.km - -// kn : https://en.wikipedia.org/wiki/.kn -// http://www.dot.kn/domainRules.html -kn -net.kn -org.kn -edu.kn -gov.kn - -// kp : http://www.kcce.kp/en_index.php -kp -com.kp -edu.kp -gov.kp -org.kp -rep.kp -tra.kp - -// kr : https://en.wikipedia.org/wiki/.kr -// see also: http://domain.nida.or.kr/eng/registration.jsp -kr -ac.kr -co.kr -es.kr -go.kr -hs.kr -kg.kr -mil.kr -ms.kr -ne.kr -or.kr -pe.kr -re.kr -sc.kr -// kr geographical names -busan.kr -chungbuk.kr -chungnam.kr -daegu.kr -daejeon.kr -gangwon.kr -gwangju.kr -gyeongbuk.kr -gyeonggi.kr -gyeongnam.kr -incheon.kr -jeju.kr -jeonbuk.kr -jeonnam.kr -seoul.kr -ulsan.kr - -// kw : https://www.nic.kw/policies/ -// Confirmed by registry -kw -com.kw -edu.kw -emb.kw -gov.kw -ind.kw -net.kw -org.kw - -// ky : http://www.icta.ky/da_ky_reg_dom.php -// Confirmed by registry 2008-06-17 -ky -com.ky -edu.ky -net.ky -org.ky - -// kz : https://en.wikipedia.org/wiki/.kz -// see also: http://www.nic.kz/rules/index.jsp -kz -org.kz -edu.kz -net.kz -gov.kz -mil.kz -com.kz - -// la : https://en.wikipedia.org/wiki/.la -// Submitted by registry -la -int.la -net.la -info.la -edu.la -gov.la -per.la -com.la -org.la - -// lb : https://en.wikipedia.org/wiki/.lb -// Submitted by registry -lb -com.lb -edu.lb -gov.lb -net.lb -org.lb - -// lc : https://en.wikipedia.org/wiki/.lc -// see also: http://www.nic.lc/rules.htm -lc -com.lc -net.lc -co.lc -org.lc -edu.lc -gov.lc - -// li : https://en.wikipedia.org/wiki/.li -li - -// lk : https://www.nic.lk/index.php/domain-registration/lk-domain-naming-structure -lk -gov.lk -sch.lk -net.lk -int.lk -com.lk -org.lk -edu.lk -ngo.lk -soc.lk -web.lk -ltd.lk -assn.lk -grp.lk -hotel.lk -ac.lk - -// lr : http://psg.com/dns/lr/lr.txt -// Submitted by registry -lr -com.lr -edu.lr -gov.lr -org.lr -net.lr - -// ls : http://www.nic.ls/ -// Confirmed by registry -ls -ac.ls -biz.ls -co.ls -edu.ls -gov.ls -info.ls -net.ls -org.ls -sc.ls - -// lt : https://en.wikipedia.org/wiki/.lt -lt -// gov.lt : http://www.gov.lt/index_en.php -gov.lt - -// lu : http://www.dns.lu/en/ -lu - -// lv : http://www.nic.lv/DNS/En/generic.php -lv -com.lv -edu.lv -gov.lv -org.lv -mil.lv -id.lv -net.lv -asn.lv -conf.lv - -// ly : http://www.nic.ly/regulations.php -ly -com.ly -net.ly -gov.ly -plc.ly -edu.ly -sch.ly -med.ly -org.ly -id.ly - -// ma : https://en.wikipedia.org/wiki/.ma -// http://www.anrt.ma/fr/admin/download/upload/file_fr782.pdf -ma -co.ma -net.ma -gov.ma -org.ma -ac.ma -press.ma - -// mc : http://www.nic.mc/ -mc -tm.mc -asso.mc - -// md : https://en.wikipedia.org/wiki/.md -md - -// me : https://en.wikipedia.org/wiki/.me -me -co.me -net.me -org.me -edu.me -ac.me -gov.me -its.me -priv.me - -// mg : http://nic.mg/nicmg/?page_id=39 -mg -org.mg -nom.mg -gov.mg -prd.mg -tm.mg -edu.mg -mil.mg -com.mg -co.mg - -// mh : https://en.wikipedia.org/wiki/.mh -mh - -// mil : https://en.wikipedia.org/wiki/.mil -mil - -// mk : https://en.wikipedia.org/wiki/.mk -// see also: http://dns.marnet.net.mk/postapka.php -mk -com.mk -org.mk -net.mk -edu.mk -gov.mk -inf.mk -name.mk - -// ml : http://www.gobin.info/domainname/ml-template.doc -// see also: https://en.wikipedia.org/wiki/.ml -ml -com.ml -edu.ml -gouv.ml -gov.ml -net.ml -org.ml -presse.ml - -// mm : https://en.wikipedia.org/wiki/.mm -*.mm - -// mn : https://en.wikipedia.org/wiki/.mn -mn -gov.mn -edu.mn -org.mn - -// mo : http://www.monic.net.mo/ -mo -com.mo -net.mo -org.mo -edu.mo -gov.mo - -// mobi : https://en.wikipedia.org/wiki/.mobi -mobi - -// mp : http://www.dot.mp/ -// Confirmed by registry 2008-06-17 -mp - -// mq : https://en.wikipedia.org/wiki/.mq -mq - -// mr : https://en.wikipedia.org/wiki/.mr -mr -gov.mr - -// ms : http://www.nic.ms/pdf/MS_Domain_Name_Rules.pdf -ms -com.ms -edu.ms -gov.ms -net.ms -org.ms - -// mt : https://www.nic.org.mt/go/policy -// Submitted by registry -mt -com.mt -edu.mt -net.mt -org.mt - -// mu : https://en.wikipedia.org/wiki/.mu -mu -com.mu -net.mu -org.mu -gov.mu -ac.mu -co.mu -or.mu - -// museum : https://welcome.museum/wp-content/uploads/2018/05/20180525-Registration-Policy-MUSEUM-EN_VF-2.pdf https://welcome.museum/buy-your-dot-museum-2/ -museum - -// mv : https://en.wikipedia.org/wiki/.mv -// "mv" included because, contra Wikipedia, google.mv exists. -mv -aero.mv -biz.mv -com.mv -coop.mv -edu.mv -gov.mv -info.mv -int.mv -mil.mv -museum.mv -name.mv -net.mv -org.mv -pro.mv - -// mw : http://www.registrar.mw/ -mw -ac.mw -biz.mw -co.mw -com.mw -coop.mw -edu.mw -gov.mw -int.mw -museum.mw -net.mw -org.mw - -// mx : http://www.nic.mx/ -// Submitted by registry -mx -com.mx -org.mx -gob.mx -edu.mx -net.mx - -// my : http://www.mynic.my/ -// Available strings: https://mynic.my/resources/domains/buying-a-domain/ -my -biz.my -com.my -edu.my -gov.my -mil.my -name.my -net.my -org.my - -// mz : http://www.uem.mz/ -// Submitted by registry -mz -ac.mz -adv.mz -co.mz -edu.mz -gov.mz -mil.mz -net.mz -org.mz - -// na : http://www.na-nic.com.na/ -// http://www.info.na/domain/ -na -info.na -pro.na -name.na -school.na -or.na -dr.na -us.na -mx.na -ca.na -in.na -cc.na -tv.na -ws.na -mobi.na -co.na -com.na -org.na - -// name : has 2nd-level tlds, but there's no list of them -name - -// nc : http://www.cctld.nc/ -nc -asso.nc -nom.nc - -// ne : https://en.wikipedia.org/wiki/.ne -ne - -// net : https://en.wikipedia.org/wiki/.net -net - -// nf : https://en.wikipedia.org/wiki/.nf -nf -com.nf -net.nf -per.nf -rec.nf -web.nf -arts.nf -firm.nf -info.nf -other.nf -store.nf - -// ng : http://www.nira.org.ng/index.php/join-us/register-ng-domain/189-nira-slds -ng -com.ng -edu.ng -gov.ng -i.ng -mil.ng -mobi.ng -name.ng -net.ng -org.ng -sch.ng - -// ni : http://www.nic.ni/ -ni -ac.ni -biz.ni -co.ni -com.ni -edu.ni -gob.ni -in.ni -info.ni -int.ni -mil.ni -net.ni -nom.ni -org.ni -web.ni - -// nl : https://en.wikipedia.org/wiki/.nl -// https://www.sidn.nl/ -// ccTLD for the Netherlands -nl - -// no : https://www.norid.no/en/om-domenenavn/regelverk-for-no/ -// Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/ -// Norid category second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-c/ -// Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/ -// RSS feed: https://teknisk.norid.no/en/feed/ -no -// Norid category second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-c/ -fhs.no -vgs.no -fylkesbibl.no -folkebibl.no -museum.no -idrett.no -priv.no -// Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/ -mil.no -stat.no -dep.no -kommune.no -herad.no -// Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/ -// counties -aa.no -ah.no -bu.no -fm.no -hl.no -hm.no -jan-mayen.no -mr.no -nl.no -nt.no -of.no -ol.no -oslo.no -rl.no -sf.no -st.no -svalbard.no -tm.no -tr.no -va.no -vf.no -// primary and lower secondary schools per county -gs.aa.no -gs.ah.no -gs.bu.no -gs.fm.no -gs.hl.no -gs.hm.no -gs.jan-mayen.no -gs.mr.no -gs.nl.no -gs.nt.no -gs.of.no -gs.ol.no -gs.oslo.no -gs.rl.no -gs.sf.no -gs.st.no -gs.svalbard.no -gs.tm.no -gs.tr.no -gs.va.no -gs.vf.no -// cities -akrehamn.no -åkrehamn.no -algard.no -ålgård.no -arna.no -brumunddal.no -bryne.no -bronnoysund.no -brønnøysund.no -drobak.no -drøbak.no -egersund.no -fetsund.no -floro.no -florø.no -fredrikstad.no -hokksund.no -honefoss.no -hønefoss.no -jessheim.no -jorpeland.no -jørpeland.no -kirkenes.no -kopervik.no -krokstadelva.no -langevag.no -langevåg.no -leirvik.no -mjondalen.no -mjøndalen.no -mo-i-rana.no -mosjoen.no -mosjøen.no -nesoddtangen.no -orkanger.no -osoyro.no -osøyro.no -raholt.no -råholt.no -sandnessjoen.no -sandnessjøen.no -skedsmokorset.no -slattum.no -spjelkavik.no -stathelle.no -stavern.no -stjordalshalsen.no -stjørdalshalsen.no -tananger.no -tranby.no -vossevangen.no -// communities -afjord.no -åfjord.no -agdenes.no -al.no -ål.no -alesund.no -ålesund.no -alstahaug.no -alta.no -áltá.no -alaheadju.no -álaheadju.no -alvdal.no -amli.no -åmli.no -amot.no -åmot.no -andebu.no -andoy.no -andøy.no -andasuolo.no -ardal.no -årdal.no -aremark.no -arendal.no -ås.no -aseral.no -åseral.no -asker.no -askim.no -askvoll.no -askoy.no -askøy.no -asnes.no -åsnes.no -audnedaln.no -aukra.no -aure.no -aurland.no -aurskog-holand.no -aurskog-høland.no -austevoll.no -austrheim.no -averoy.no -averøy.no -balestrand.no -ballangen.no -balat.no -bálát.no -balsfjord.no -bahccavuotna.no -báhccavuotna.no -bamble.no -bardu.no -beardu.no -beiarn.no -bajddar.no -bájddar.no -baidar.no -báidár.no -berg.no -bergen.no -berlevag.no -berlevåg.no -bearalvahki.no -bearalváhki.no -bindal.no -birkenes.no -bjarkoy.no -bjarkøy.no -bjerkreim.no -bjugn.no -bodo.no -bodø.no -badaddja.no -bådåddjå.no -budejju.no -bokn.no -bremanger.no -bronnoy.no -brønnøy.no -bygland.no -bykle.no -barum.no -bærum.no -bo.telemark.no -bø.telemark.no -bo.nordland.no -bø.nordland.no -bievat.no -bievát.no -bomlo.no -bømlo.no -batsfjord.no -båtsfjord.no -bahcavuotna.no -báhcavuotna.no -dovre.no -drammen.no -drangedal.no -dyroy.no -dyrøy.no -donna.no -dønna.no -eid.no -eidfjord.no -eidsberg.no -eidskog.no -eidsvoll.no -eigersund.no -elverum.no -enebakk.no -engerdal.no -etne.no -etnedal.no -evenes.no -evenassi.no -evenášši.no -evje-og-hornnes.no -farsund.no -fauske.no -fuossko.no -fuoisku.no -fedje.no -fet.no -finnoy.no -finnøy.no -fitjar.no -fjaler.no -fjell.no -flakstad.no -flatanger.no -flekkefjord.no -flesberg.no -flora.no -fla.no -flå.no -folldal.no -forsand.no -fosnes.no -frei.no -frogn.no -froland.no -frosta.no -frana.no -fræna.no -froya.no -frøya.no -fusa.no -fyresdal.no -forde.no -førde.no -gamvik.no -gangaviika.no -gáŋgaviika.no -gaular.no -gausdal.no -gildeskal.no -gildeskål.no -giske.no -gjemnes.no -gjerdrum.no -gjerstad.no -gjesdal.no -gjovik.no -gjøvik.no -gloppen.no -gol.no -gran.no -grane.no -granvin.no -gratangen.no -grimstad.no -grong.no -kraanghke.no -kråanghke.no -grue.no -gulen.no -hadsel.no -halden.no -halsa.no -hamar.no -hamaroy.no -habmer.no -hábmer.no -hapmir.no -hápmir.no -hammerfest.no -hammarfeasta.no -hámmárfeasta.no -haram.no -hareid.no -harstad.no -hasvik.no -aknoluokta.no -ákŋoluokta.no -hattfjelldal.no -aarborte.no -haugesund.no -hemne.no -hemnes.no -hemsedal.no -heroy.more-og-romsdal.no -herøy.møre-og-romsdal.no -heroy.nordland.no -herøy.nordland.no -hitra.no -hjartdal.no -hjelmeland.no -hobol.no -hobøl.no -hof.no -hol.no -hole.no -holmestrand.no -holtalen.no -holtålen.no -hornindal.no -horten.no -hurdal.no -hurum.no -hvaler.no -hyllestad.no -hagebostad.no -hægebostad.no -hoyanger.no -høyanger.no -hoylandet.no -høylandet.no -ha.no -hå.no -ibestad.no -inderoy.no -inderøy.no -iveland.no -jevnaker.no -jondal.no -jolster.no -jølster.no -karasjok.no -karasjohka.no -kárášjohka.no -karlsoy.no -galsa.no -gálsá.no -karmoy.no -karmøy.no -kautokeino.no -guovdageaidnu.no -klepp.no -klabu.no -klæbu.no -kongsberg.no -kongsvinger.no -kragero.no -kragerø.no -kristiansand.no -kristiansund.no -krodsherad.no -krødsherad.no -kvalsund.no -rahkkeravju.no -ráhkkerávju.no -kvam.no -kvinesdal.no -kvinnherad.no -kviteseid.no -kvitsoy.no -kvitsøy.no -kvafjord.no -kvæfjord.no -giehtavuoatna.no -kvanangen.no -kvænangen.no -navuotna.no -návuotna.no -kafjord.no -kåfjord.no -gaivuotna.no -gáivuotna.no -larvik.no -lavangen.no -lavagis.no -loabat.no -loabát.no -lebesby.no -davvesiida.no -leikanger.no -leirfjord.no -leka.no -leksvik.no -lenvik.no -leangaviika.no -leaŋgaviika.no -lesja.no -levanger.no -lier.no -lierne.no -lillehammer.no -lillesand.no -lindesnes.no -lindas.no -lindås.no -lom.no -loppa.no -lahppi.no -láhppi.no -lund.no -lunner.no -luroy.no -lurøy.no -luster.no -lyngdal.no -lyngen.no -ivgu.no -lardal.no -lerdal.no -lærdal.no -lodingen.no -lødingen.no -lorenskog.no -lørenskog.no -loten.no -løten.no -malvik.no -masoy.no -måsøy.no -muosat.no -muosát.no -mandal.no -marker.no -marnardal.no -masfjorden.no -meland.no -meldal.no -melhus.no -meloy.no -meløy.no -meraker.no -meråker.no -moareke.no -moåreke.no -midsund.no -midtre-gauldal.no -modalen.no -modum.no -molde.no -moskenes.no -moss.no -mosvik.no -malselv.no -målselv.no -malatvuopmi.no -málatvuopmi.no -namdalseid.no -aejrie.no -namsos.no -namsskogan.no -naamesjevuemie.no -nååmesjevuemie.no -laakesvuemie.no -nannestad.no -narvik.no -narviika.no -naustdal.no -nedre-eiker.no -nes.akershus.no -nes.buskerud.no -nesna.no -nesodden.no -nesseby.no -unjarga.no -unjárga.no -nesset.no -nissedal.no -nittedal.no -nord-aurdal.no -nord-fron.no -nord-odal.no -norddal.no -nordkapp.no -davvenjarga.no -davvenjárga.no -nordre-land.no -nordreisa.no -raisa.no -ráisa.no -nore-og-uvdal.no -notodden.no -naroy.no -nærøy.no -notteroy.no -nøtterøy.no -odda.no -oksnes.no -øksnes.no -oppdal.no -oppegard.no -oppegård.no -orkdal.no -orland.no -ørland.no -orskog.no -ørskog.no -orsta.no -ørsta.no -os.hedmark.no -os.hordaland.no -osen.no -osteroy.no -osterøy.no -ostre-toten.no -østre-toten.no -overhalla.no -ovre-eiker.no -øvre-eiker.no -oyer.no -øyer.no -oygarden.no -øygarden.no -oystre-slidre.no -øystre-slidre.no -porsanger.no -porsangu.no -porsáŋgu.no -porsgrunn.no -radoy.no -radøy.no -rakkestad.no -rana.no -ruovat.no -randaberg.no -rauma.no -rendalen.no -rennebu.no -rennesoy.no -rennesøy.no -rindal.no -ringebu.no -ringerike.no -ringsaker.no -rissa.no -risor.no -risør.no -roan.no -rollag.no -rygge.no -ralingen.no -rælingen.no -rodoy.no -rødøy.no -romskog.no -rømskog.no -roros.no -røros.no -rost.no -røst.no -royken.no -røyken.no -royrvik.no -røyrvik.no -rade.no -råde.no -salangen.no -siellak.no -saltdal.no -salat.no -sálát.no -sálat.no -samnanger.no -sande.more-og-romsdal.no -sande.møre-og-romsdal.no -sande.vestfold.no -sandefjord.no -sandnes.no -sandoy.no -sandøy.no -sarpsborg.no -sauda.no -sauherad.no -sel.no -selbu.no -selje.no -seljord.no -sigdal.no -siljan.no -sirdal.no -skaun.no -skedsmo.no -ski.no -skien.no -skiptvet.no -skjervoy.no -skjervøy.no -skierva.no -skiervá.no -skjak.no -skjåk.no -skodje.no -skanland.no -skånland.no -skanit.no -skánit.no -smola.no -smøla.no -snillfjord.no -snasa.no -snåsa.no -snoasa.no -snaase.no -snåase.no -sogndal.no -sokndal.no -sola.no -solund.no -songdalen.no -sortland.no -spydeberg.no -stange.no -stavanger.no -steigen.no -steinkjer.no -stjordal.no -stjørdal.no -stokke.no -stor-elvdal.no -stord.no -stordal.no -storfjord.no -omasvuotna.no -strand.no -stranda.no -stryn.no -sula.no -suldal.no -sund.no -sunndal.no -surnadal.no -sveio.no -svelvik.no -sykkylven.no -sogne.no -søgne.no -somna.no -sømna.no -sondre-land.no -søndre-land.no -sor-aurdal.no -sør-aurdal.no -sor-fron.no -sør-fron.no -sor-odal.no -sør-odal.no -sor-varanger.no -sør-varanger.no -matta-varjjat.no -mátta-várjjat.no -sorfold.no -sørfold.no -sorreisa.no -sørreisa.no -sorum.no -sørum.no -tana.no -deatnu.no -time.no -tingvoll.no -tinn.no -tjeldsund.no -dielddanuorri.no -tjome.no -tjøme.no -tokke.no -tolga.no -torsken.no -tranoy.no -tranøy.no -tromso.no -tromsø.no -tromsa.no -romsa.no -trondheim.no -troandin.no -trysil.no -trana.no -træna.no -trogstad.no -trøgstad.no -tvedestrand.no -tydal.no -tynset.no -tysfjord.no -divtasvuodna.no -divttasvuotna.no -tysnes.no -tysvar.no -tysvær.no -tonsberg.no -tønsberg.no -ullensaker.no -ullensvang.no -ulvik.no -utsira.no -vadso.no -vadsø.no -cahcesuolo.no -čáhcesuolo.no -vaksdal.no -valle.no -vang.no -vanylven.no -vardo.no -vardø.no -varggat.no -várggát.no -vefsn.no -vaapste.no -vega.no -vegarshei.no -vegårshei.no -vennesla.no -verdal.no -verran.no -vestby.no -vestnes.no -vestre-slidre.no -vestre-toten.no -vestvagoy.no -vestvågøy.no -vevelstad.no -vik.no -vikna.no -vindafjord.no -volda.no -voss.no -varoy.no -værøy.no -vagan.no -vågan.no -voagat.no -vagsoy.no -vågsøy.no -vaga.no -vågå.no -valer.ostfold.no -våler.østfold.no -valer.hedmark.no -våler.hedmark.no - -// np : http://www.mos.com.np/register.html -*.np - -// nr : http://cenpac.net.nr/dns/index.html -// Submitted by registry -nr -biz.nr -info.nr -gov.nr -edu.nr -org.nr -net.nr -com.nr - -// nu : https://en.wikipedia.org/wiki/.nu -nu - -// nz : https://en.wikipedia.org/wiki/.nz -// Submitted by registry -nz -ac.nz -co.nz -cri.nz -geek.nz -gen.nz -govt.nz -health.nz -iwi.nz -kiwi.nz -maori.nz -mil.nz -māori.nz -net.nz -org.nz -parliament.nz -school.nz - -// om : https://en.wikipedia.org/wiki/.om -om -co.om -com.om -edu.om -gov.om -med.om -museum.om -net.om -org.om -pro.om - -// onion : https://tools.ietf.org/html/rfc7686 -onion - -// org : https://en.wikipedia.org/wiki/.org -org - -// pa : http://www.nic.pa/ -// Some additional second level "domains" resolve directly as hostnames, such as -// pannet.pa, so we add a rule for "pa". -pa -ac.pa -gob.pa -com.pa -org.pa -sld.pa -edu.pa -net.pa -ing.pa -abo.pa -med.pa -nom.pa - -// pe : https://www.nic.pe/InformeFinalComision.pdf -pe -edu.pe -gob.pe -nom.pe -mil.pe -org.pe -com.pe -net.pe - -// pf : http://www.gobin.info/domainname/formulaire-pf.pdf -pf -com.pf -org.pf -edu.pf - -// pg : https://en.wikipedia.org/wiki/.pg -*.pg - -// ph : http://www.domains.ph/FAQ2.asp -// Submitted by registry -ph -com.ph -net.ph -org.ph -gov.ph -edu.ph -ngo.ph -mil.ph -i.ph - -// pk : http://pk5.pknic.net.pk/pk5/msgNamepk.PK -pk -com.pk -net.pk -edu.pk -org.pk -fam.pk -biz.pk -web.pk -gov.pk -gob.pk -gok.pk -gon.pk -gop.pk -gos.pk -info.pk - -// pl http://www.dns.pl/english/index.html -// Submitted by registry -pl -com.pl -net.pl -org.pl -// pl functional domains (http://www.dns.pl/english/index.html) -aid.pl -agro.pl -atm.pl -auto.pl -biz.pl -edu.pl -gmina.pl -gsm.pl -info.pl -mail.pl -miasta.pl -media.pl -mil.pl -nieruchomosci.pl -nom.pl -pc.pl -powiat.pl -priv.pl -realestate.pl -rel.pl -sex.pl -shop.pl -sklep.pl -sos.pl -szkola.pl -targi.pl -tm.pl -tourism.pl -travel.pl -turystyka.pl -// Government domains -gov.pl -ap.gov.pl -ic.gov.pl -is.gov.pl -us.gov.pl -kmpsp.gov.pl -kppsp.gov.pl -kwpsp.gov.pl -psp.gov.pl -wskr.gov.pl -kwp.gov.pl -mw.gov.pl -ug.gov.pl -um.gov.pl -umig.gov.pl -ugim.gov.pl -upow.gov.pl -uw.gov.pl -starostwo.gov.pl -pa.gov.pl -po.gov.pl -psse.gov.pl -pup.gov.pl -rzgw.gov.pl -sa.gov.pl -so.gov.pl -sr.gov.pl -wsa.gov.pl -sko.gov.pl -uzs.gov.pl -wiih.gov.pl -winb.gov.pl -pinb.gov.pl -wios.gov.pl -witd.gov.pl -wzmiuw.gov.pl -piw.gov.pl -wiw.gov.pl -griw.gov.pl -wif.gov.pl -oum.gov.pl -sdn.gov.pl -zp.gov.pl -uppo.gov.pl -mup.gov.pl -wuoz.gov.pl -konsulat.gov.pl -oirm.gov.pl -// pl regional domains (http://www.dns.pl/english/index.html) -augustow.pl -babia-gora.pl -bedzin.pl -beskidy.pl -bialowieza.pl -bialystok.pl -bielawa.pl -bieszczady.pl -boleslawiec.pl -bydgoszcz.pl -bytom.pl -cieszyn.pl -czeladz.pl -czest.pl -dlugoleka.pl -elblag.pl -elk.pl -glogow.pl -gniezno.pl -gorlice.pl -grajewo.pl -ilawa.pl -jaworzno.pl -jelenia-gora.pl -jgora.pl -kalisz.pl -kazimierz-dolny.pl -karpacz.pl -kartuzy.pl -kaszuby.pl -katowice.pl -kepno.pl -ketrzyn.pl -klodzko.pl -kobierzyce.pl -kolobrzeg.pl -konin.pl -konskowola.pl -kutno.pl -lapy.pl -lebork.pl -legnica.pl -lezajsk.pl -limanowa.pl -lomza.pl -lowicz.pl -lubin.pl -lukow.pl -malbork.pl -malopolska.pl -mazowsze.pl -mazury.pl -mielec.pl -mielno.pl -mragowo.pl -naklo.pl -nowaruda.pl -nysa.pl -olawa.pl -olecko.pl -olkusz.pl -olsztyn.pl -opoczno.pl -opole.pl -ostroda.pl -ostroleka.pl -ostrowiec.pl -ostrowwlkp.pl -pila.pl -pisz.pl -podhale.pl -podlasie.pl -polkowice.pl -pomorze.pl -pomorskie.pl -prochowice.pl -pruszkow.pl -przeworsk.pl -pulawy.pl -radom.pl -rawa-maz.pl -rybnik.pl -rzeszow.pl -sanok.pl -sejny.pl -slask.pl -slupsk.pl -sosnowiec.pl -stalowa-wola.pl -skoczow.pl -starachowice.pl -stargard.pl -suwalki.pl -swidnica.pl -swiebodzin.pl -swinoujscie.pl -szczecin.pl -szczytno.pl -tarnobrzeg.pl -tgory.pl -turek.pl -tychy.pl -ustka.pl -walbrzych.pl -warmia.pl -warszawa.pl -waw.pl -wegrow.pl -wielun.pl -wlocl.pl -wloclawek.pl -wodzislaw.pl -wolomin.pl -wroclaw.pl -zachpomor.pl -zagan.pl -zarow.pl -zgora.pl -zgorzelec.pl - -// pm : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf -pm - -// pn : http://www.government.pn/PnRegistry/policies.htm -pn -gov.pn -co.pn -org.pn -edu.pn -net.pn - -// post : https://en.wikipedia.org/wiki/.post -post - -// pr : http://www.nic.pr/index.asp?f=1 -pr -com.pr -net.pr -org.pr -gov.pr -edu.pr -isla.pr -pro.pr -biz.pr -info.pr -name.pr -// these aren't mentioned on nic.pr, but on https://en.wikipedia.org/wiki/.pr -est.pr -prof.pr -ac.pr - -// pro : http://registry.pro/get-pro -pro -aaa.pro -aca.pro -acct.pro -avocat.pro -bar.pro -cpa.pro -eng.pro -jur.pro -law.pro -med.pro -recht.pro - -// ps : https://en.wikipedia.org/wiki/.ps -// http://www.nic.ps/registration/policy.html#reg -ps -edu.ps -gov.ps -sec.ps -plo.ps -com.ps -org.ps -net.ps - -// pt : https://www.dns.pt/en/domain/pt-terms-and-conditions-registration-rules/ -pt -net.pt -gov.pt -org.pt -edu.pt -int.pt -publ.pt -com.pt -nome.pt - -// pw : https://en.wikipedia.org/wiki/.pw -pw -co.pw -ne.pw -or.pw -ed.pw -go.pw -belau.pw - -// py : http://www.nic.py/pautas.html#seccion_9 -// Submitted by registry -py -com.py -coop.py -edu.py -gov.py -mil.py -net.py -org.py - -// qa : http://domains.qa/en/ -qa -com.qa -edu.qa -gov.qa -mil.qa -name.qa -net.qa -org.qa -sch.qa - -// re : http://www.afnic.re/obtenir/chartes/nommage-re/annexe-descriptifs -re -asso.re -com.re -nom.re - -// ro : http://www.rotld.ro/ -ro -arts.ro -com.ro -firm.ro -info.ro -nom.ro -nt.ro -org.ro -rec.ro -store.ro -tm.ro -www.ro - -// rs : https://www.rnids.rs/en/domains/national-domains -rs -ac.rs -co.rs -edu.rs -gov.rs -in.rs -org.rs - -// ru : https://cctld.ru/files/pdf/docs/en/rules_ru-rf.pdf -// Submitted by George Georgievsky -ru - -// rw : https://www.ricta.org.rw/sites/default/files/resources/registry_registrar_contract_0.pdf -rw -ac.rw -co.rw -coop.rw -gov.rw -mil.rw -net.rw -org.rw - -// sa : http://www.nic.net.sa/ -sa -com.sa -net.sa -org.sa -gov.sa -med.sa -pub.sa -edu.sa -sch.sa - -// sb : http://www.sbnic.net.sb/ -// Submitted by registry -sb -com.sb -edu.sb -gov.sb -net.sb -org.sb - -// sc : http://www.nic.sc/ -sc -com.sc -gov.sc -net.sc -org.sc -edu.sc - -// sd : http://www.isoc.sd/sudanic.isoc.sd/billing_pricing.htm -// Submitted by registry -sd -com.sd -net.sd -org.sd -edu.sd -med.sd -tv.sd -gov.sd -info.sd - -// se : https://en.wikipedia.org/wiki/.se -// Submitted by registry -se -a.se -ac.se -b.se -bd.se -brand.se -c.se -d.se -e.se -f.se -fh.se -fhsk.se -fhv.se -g.se -h.se -i.se -k.se -komforb.se -kommunalforbund.se -komvux.se -l.se -lanbib.se -m.se -n.se -naturbruksgymn.se -o.se -org.se -p.se -parti.se -pp.se -press.se -r.se -s.se -t.se -tm.se -u.se -w.se -x.se -y.se -z.se - -// sg : http://www.nic.net.sg/page/registration-policies-procedures-and-guidelines -sg -com.sg -net.sg -org.sg -gov.sg -edu.sg -per.sg - -// sh : http://nic.sh/rules.htm -sh -com.sh -net.sh -gov.sh -org.sh -mil.sh - -// si : https://en.wikipedia.org/wiki/.si -si - -// sj : No registrations at this time. -// Submitted by registry -sj - -// sk : https://en.wikipedia.org/wiki/.sk -// list of 2nd level domains ? -sk - -// sl : http://www.nic.sl -// Submitted by registry -sl -com.sl -net.sl -edu.sl -gov.sl -org.sl - -// sm : https://en.wikipedia.org/wiki/.sm -sm - -// sn : https://en.wikipedia.org/wiki/.sn -sn -art.sn -com.sn -edu.sn -gouv.sn -org.sn -perso.sn -univ.sn - -// so : http://sonic.so/policies/ -so -com.so -edu.so -gov.so -me.so -net.so -org.so - -// sr : https://en.wikipedia.org/wiki/.sr -sr - -// ss : https://registry.nic.ss/ -// Submitted by registry -ss -biz.ss -com.ss -edu.ss -gov.ss -me.ss -net.ss -org.ss -sch.ss - -// st : http://www.nic.st/html/policyrules/ -st -co.st -com.st -consulado.st -edu.st -embaixada.st -mil.st -net.st -org.st -principe.st -saotome.st -store.st - -// su : https://en.wikipedia.org/wiki/.su -su - -// sv : http://www.svnet.org.sv/niveldos.pdf -sv -com.sv -edu.sv -gob.sv -org.sv -red.sv - -// sx : https://en.wikipedia.org/wiki/.sx -// Submitted by registry -sx -gov.sx - -// sy : https://en.wikipedia.org/wiki/.sy -// see also: http://www.gobin.info/domainname/sy.doc -sy -edu.sy -gov.sy -net.sy -mil.sy -com.sy -org.sy - -// sz : https://en.wikipedia.org/wiki/.sz -// http://www.sispa.org.sz/ -sz -co.sz -ac.sz -org.sz - -// tc : https://en.wikipedia.org/wiki/.tc -tc - -// td : https://en.wikipedia.org/wiki/.td -td - -// tel: https://en.wikipedia.org/wiki/.tel -// http://www.telnic.org/ -tel - -// tf : https://en.wikipedia.org/wiki/.tf -tf - -// tg : https://en.wikipedia.org/wiki/.tg -// http://www.nic.tg/ -tg - -// th : https://en.wikipedia.org/wiki/.th -// Submitted by registry -th -ac.th -co.th -go.th -in.th -mi.th -net.th -or.th - -// tj : http://www.nic.tj/policy.html -tj -ac.tj -biz.tj -co.tj -com.tj -edu.tj -go.tj -gov.tj -int.tj -mil.tj -name.tj -net.tj -nic.tj -org.tj -test.tj -web.tj - -// tk : https://en.wikipedia.org/wiki/.tk -tk - -// tl : https://en.wikipedia.org/wiki/.tl -tl -gov.tl - -// tm : http://www.nic.tm/local.html -tm -com.tm -co.tm -org.tm -net.tm -nom.tm -gov.tm -mil.tm -edu.tm - -// tn : http://www.registre.tn/fr/ -// https://whois.ati.tn/ -tn -com.tn -ens.tn -fin.tn -gov.tn -ind.tn -info.tn -intl.tn -mincom.tn -nat.tn -net.tn -org.tn -perso.tn -tourism.tn - -// to : https://en.wikipedia.org/wiki/.to -// Submitted by registry -to -com.to -gov.to -net.to -org.to -edu.to -mil.to - -// tr : https://nic.tr/ -// https://nic.tr/forms/eng/policies.pdf -// https://nic.tr/index.php?USRACTN=PRICELST -tr -av.tr -bbs.tr -bel.tr -biz.tr -com.tr -dr.tr -edu.tr -gen.tr -gov.tr -info.tr -mil.tr -k12.tr -kep.tr -name.tr -net.tr -org.tr -pol.tr -tel.tr -tsk.tr -tv.tr -web.tr -// Used by Northern Cyprus -nc.tr -// Used by government agencies of Northern Cyprus -gov.nc.tr - -// tt : http://www.nic.tt/ -tt -co.tt -com.tt -org.tt -net.tt -biz.tt -info.tt -pro.tt -int.tt -coop.tt -jobs.tt -mobi.tt -travel.tt -museum.tt -aero.tt -name.tt -gov.tt -edu.tt - -// tv : https://en.wikipedia.org/wiki/.tv -// Not listing any 2LDs as reserved since none seem to exist in practice, -// Wikipedia notwithstanding. -tv - -// tw : https://en.wikipedia.org/wiki/.tw -tw -edu.tw -gov.tw -mil.tw -com.tw -net.tw -org.tw -idv.tw -game.tw -ebiz.tw -club.tw -網路.tw -組織.tw -商業.tw - -// tz : http://www.tznic.or.tz/index.php/domains -// Submitted by registry -tz -ac.tz -co.tz -go.tz -hotel.tz -info.tz -me.tz -mil.tz -mobi.tz -ne.tz -or.tz -sc.tz -tv.tz - -// ua : https://hostmaster.ua/policy/?ua -// Submitted by registry -ua -// ua 2LD -com.ua -edu.ua -gov.ua -in.ua -net.ua -org.ua -// ua geographic names -// https://hostmaster.ua/2ld/ -cherkassy.ua -cherkasy.ua -chernigov.ua -chernihiv.ua -chernivtsi.ua -chernovtsy.ua -ck.ua -cn.ua -cr.ua -crimea.ua -cv.ua -dn.ua -dnepropetrovsk.ua -dnipropetrovsk.ua -donetsk.ua -dp.ua -if.ua -ivano-frankivsk.ua -kh.ua -kharkiv.ua -kharkov.ua -kherson.ua -khmelnitskiy.ua -khmelnytskyi.ua -kiev.ua -kirovograd.ua -km.ua -kr.ua -krym.ua -ks.ua -kv.ua -kyiv.ua -lg.ua -lt.ua -lugansk.ua -lutsk.ua -lv.ua -lviv.ua -mk.ua -mykolaiv.ua -nikolaev.ua -od.ua -odesa.ua -odessa.ua -pl.ua -poltava.ua -rivne.ua -rovno.ua -rv.ua -sb.ua -sebastopol.ua -sevastopol.ua -sm.ua -sumy.ua -te.ua -ternopil.ua -uz.ua -uzhgorod.ua -vinnica.ua -vinnytsia.ua -vn.ua -volyn.ua -yalta.ua -zaporizhzhe.ua -zaporizhzhia.ua -zhitomir.ua -zhytomyr.ua -zp.ua -zt.ua - -// ug : https://www.registry.co.ug/ -ug -co.ug -or.ug -ac.ug -sc.ug -go.ug -ne.ug -com.ug -org.ug - -// uk : https://en.wikipedia.org/wiki/.uk -// Submitted by registry -uk -ac.uk -co.uk -gov.uk -ltd.uk -me.uk -net.uk -nhs.uk -org.uk -plc.uk -police.uk -*.sch.uk - -// us : https://en.wikipedia.org/wiki/.us -us -dni.us -fed.us -isa.us -kids.us -nsn.us -// us geographic names -ak.us -al.us -ar.us -as.us -az.us -ca.us -co.us -ct.us -dc.us -de.us -fl.us -ga.us -gu.us -hi.us -ia.us -id.us -il.us -in.us -ks.us -ky.us -la.us -ma.us -md.us -me.us -mi.us -mn.us -mo.us -ms.us -mt.us -nc.us -nd.us -ne.us -nh.us -nj.us -nm.us -nv.us -ny.us -oh.us -ok.us -or.us -pa.us -pr.us -ri.us -sc.us -sd.us -tn.us -tx.us -ut.us -vi.us -vt.us -va.us -wa.us -wi.us -wv.us -wy.us -// The registrar notes several more specific domains available in each state, -// such as state.*.us, dst.*.us, etc., but resolution of these is somewhat -// haphazard; in some states these domains resolve as addresses, while in others -// only subdomains are available, or even nothing at all. We include the -// most common ones where it's clear that different sites are different -// entities. -k12.ak.us -k12.al.us -k12.ar.us -k12.as.us -k12.az.us -k12.ca.us -k12.co.us -k12.ct.us -k12.dc.us -k12.de.us -k12.fl.us -k12.ga.us -k12.gu.us -// k12.hi.us Bug 614565 - Hawaii has a state-wide DOE login -k12.ia.us -k12.id.us -k12.il.us -k12.in.us -k12.ks.us -k12.ky.us -k12.la.us -k12.ma.us -k12.md.us -k12.me.us -k12.mi.us -k12.mn.us -k12.mo.us -k12.ms.us -k12.mt.us -k12.nc.us -// k12.nd.us Bug 1028347 - Removed at request of Travis Rosso -k12.ne.us -k12.nh.us -k12.nj.us -k12.nm.us -k12.nv.us -k12.ny.us -k12.oh.us -k12.ok.us -k12.or.us -k12.pa.us -k12.pr.us -// k12.ri.us Removed at request of Kim Cournoyer -k12.sc.us -// k12.sd.us Bug 934131 - Removed at request of James Booze -k12.tn.us -k12.tx.us -k12.ut.us -k12.vi.us -k12.vt.us -k12.va.us -k12.wa.us -k12.wi.us -// k12.wv.us Bug 947705 - Removed at request of Verne Britton -k12.wy.us -cc.ak.us -cc.al.us -cc.ar.us -cc.as.us -cc.az.us -cc.ca.us -cc.co.us -cc.ct.us -cc.dc.us -cc.de.us -cc.fl.us -cc.ga.us -cc.gu.us -cc.hi.us -cc.ia.us -cc.id.us -cc.il.us -cc.in.us -cc.ks.us -cc.ky.us -cc.la.us -cc.ma.us -cc.md.us -cc.me.us -cc.mi.us -cc.mn.us -cc.mo.us -cc.ms.us -cc.mt.us -cc.nc.us -cc.nd.us -cc.ne.us -cc.nh.us -cc.nj.us -cc.nm.us -cc.nv.us -cc.ny.us -cc.oh.us -cc.ok.us -cc.or.us -cc.pa.us -cc.pr.us -cc.ri.us -cc.sc.us -cc.sd.us -cc.tn.us -cc.tx.us -cc.ut.us -cc.vi.us -cc.vt.us -cc.va.us -cc.wa.us -cc.wi.us -cc.wv.us -cc.wy.us -lib.ak.us -lib.al.us -lib.ar.us -lib.as.us -lib.az.us -lib.ca.us -lib.co.us -lib.ct.us -lib.dc.us -// lib.de.us Issue #243 - Moved to Private section at request of Ed Moore -lib.fl.us -lib.ga.us -lib.gu.us -lib.hi.us -lib.ia.us -lib.id.us -lib.il.us -lib.in.us -lib.ks.us -lib.ky.us -lib.la.us -lib.ma.us -lib.md.us -lib.me.us -lib.mi.us -lib.mn.us -lib.mo.us -lib.ms.us -lib.mt.us -lib.nc.us -lib.nd.us -lib.ne.us -lib.nh.us -lib.nj.us -lib.nm.us -lib.nv.us -lib.ny.us -lib.oh.us -lib.ok.us -lib.or.us -lib.pa.us -lib.pr.us -lib.ri.us -lib.sc.us -lib.sd.us -lib.tn.us -lib.tx.us -lib.ut.us -lib.vi.us -lib.vt.us -lib.va.us -lib.wa.us -lib.wi.us -// lib.wv.us Bug 941670 - Removed at request of Larry W Arnold -lib.wy.us -// k12.ma.us contains school districts in Massachusetts. The 4LDs are -// managed independently except for private (PVT), charter (CHTR) and -// parochial (PAROCH) schools. Those are delegated directly to the -// 5LD operators. -pvt.k12.ma.us -chtr.k12.ma.us -paroch.k12.ma.us -// Merit Network, Inc. maintains the registry for =~ /(k12|cc|lib).mi.us/ and the following -// see also: http://domreg.merit.edu -// see also: whois -h whois.domreg.merit.edu help -ann-arbor.mi.us -cog.mi.us -dst.mi.us -eaton.mi.us -gen.mi.us -mus.mi.us -tec.mi.us -washtenaw.mi.us - -// uy : http://www.nic.org.uy/ -uy -com.uy -edu.uy -gub.uy -mil.uy -net.uy -org.uy - -// uz : http://www.reg.uz/ -uz -co.uz -com.uz -net.uz -org.uz - -// va : https://en.wikipedia.org/wiki/.va -va - -// vc : https://en.wikipedia.org/wiki/.vc -// Submitted by registry -vc -com.vc -net.vc -org.vc -gov.vc -mil.vc -edu.vc - -// ve : https://registro.nic.ve/ -// Submitted by registry nic@nic.ve and nicve@conatel.gob.ve -ve -arts.ve -bib.ve -co.ve -com.ve -e12.ve -edu.ve -firm.ve -gob.ve -gov.ve -info.ve -int.ve -mil.ve -net.ve -nom.ve -org.ve -rar.ve -rec.ve -store.ve -tec.ve -web.ve - -// vg : https://en.wikipedia.org/wiki/.vg -vg - -// vi : http://www.nic.vi/newdomainform.htm -// http://www.nic.vi/Domain_Rules/body_domain_rules.html indicates some other -// TLDs are "reserved", such as edu.vi and gov.vi, but doesn't actually say they -// are available for registration (which they do not seem to be). -vi -co.vi -com.vi -k12.vi -net.vi -org.vi - -// vn : https://www.dot.vn/vnnic/vnnic/domainregistration.jsp -vn -com.vn -net.vn -org.vn -edu.vn -gov.vn -int.vn -ac.vn -biz.vn -info.vn -name.vn -pro.vn -health.vn - -// vu : https://en.wikipedia.org/wiki/.vu -// http://www.vunic.vu/ -vu -com.vu -edu.vu -net.vu -org.vu - -// wf : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf -wf - -// ws : https://en.wikipedia.org/wiki/.ws -// http://samoanic.ws/index.dhtml -ws -com.ws -net.ws -org.ws -gov.ws -edu.ws - -// yt : http://www.afnic.fr/medias/documents/AFNIC-naming-policy2012.pdf -yt - -// IDN ccTLDs -// When submitting patches, please maintain a sort by ISO 3166 ccTLD, then -// U-label, and follow this format: -// // A-Label ("", [, variant info]) : -// // [sponsoring org] -// U-Label - -// xn--mgbaam7a8h ("Emerat", Arabic) : AE -// http://nic.ae/english/arabicdomain/rules.jsp -امارات - -// xn--y9a3aq ("hye", Armenian) : AM -// ISOC AM (operated by .am Registry) -հայ - -// xn--54b7fta0cc ("Bangla", Bangla) : BD -বাংলা - -// xn--90ae ("bg", Bulgarian) : BG -бг - -// xn--mgbcpq6gpa1a ("albahrain", Arabic) : BH -البحرين - -// xn--90ais ("bel", Belarusian/Russian Cyrillic) : BY -// Operated by .by registry -бел - -// xn--fiqs8s ("Zhongguo/China", Chinese, Simplified) : CN -// CNNIC -// http://cnnic.cn/html/Dir/2005/10/11/3218.htm -中国 - -// xn--fiqz9s ("Zhongguo/China", Chinese, Traditional) : CN -// CNNIC -// http://cnnic.cn/html/Dir/2005/10/11/3218.htm -中國 - -// xn--lgbbat1ad8j ("Algeria/Al Jazair", Arabic) : DZ -الجزائر - -// xn--wgbh1c ("Egypt/Masr", Arabic) : EG -// http://www.dotmasr.eg/ -مصر - -// xn--e1a4c ("eu", Cyrillic) : EU -// https://eurid.eu -ею - -// xn--qxa6a ("eu", Greek) : EU -// https://eurid.eu -ευ - -// xn--mgbah1a3hjkrd ("Mauritania", Arabic) : MR -موريتانيا - -// xn--node ("ge", Georgian Mkhedruli) : GE -გე - -// xn--qxam ("el", Greek) : GR -// Hellenic Ministry of Infrastructure, Transport, and Networks -ελ - -// xn--j6w193g ("Hong Kong", Chinese) : HK -// https://www.hkirc.hk -// Submitted by registry -// https://www.hkirc.hk/content.jsp?id=30#!/34 -香港 -公司.香港 -教育.香港 -政府.香港 -個人.香港 -網絡.香港 -組織.香港 - -// xn--2scrj9c ("Bharat", Kannada) : IN -// India -ಭಾರತ - -// xn--3hcrj9c ("Bharat", Oriya) : IN -// India -ଭାରତ - -// xn--45br5cyl ("Bharatam", Assamese) : IN -// India -ভাৰত - -// xn--h2breg3eve ("Bharatam", Sanskrit) : IN -// India -भारतम् - -// xn--h2brj9c8c ("Bharot", Santali) : IN -// India -भारोत - -// xn--mgbgu82a ("Bharat", Sindhi) : IN -// India -ڀارت - -// xn--rvc1e0am3e ("Bharatam", Malayalam) : IN -// India -ഭാരതം - -// xn--h2brj9c ("Bharat", Devanagari) : IN -// India -भारत - -// xn--mgbbh1a ("Bharat", Kashmiri) : IN -// India -بارت - -// xn--mgbbh1a71e ("Bharat", Arabic) : IN -// India -بھارت - -// xn--fpcrj9c3d ("Bharat", Telugu) : IN -// India -భారత్ - -// xn--gecrj9c ("Bharat", Gujarati) : IN -// India -ભારત - -// xn--s9brj9c ("Bharat", Gurmukhi) : IN -// India -ਭਾਰਤ - -// xn--45brj9c ("Bharat", Bengali) : IN -// India -ভারত - -// xn--xkc2dl3a5ee0h ("India", Tamil) : IN -// India -இந்தியா - -// xn--mgba3a4f16a ("Iran", Persian) : IR -ایران - -// xn--mgba3a4fra ("Iran", Arabic) : IR -ايران - -// xn--mgbtx2b ("Iraq", Arabic) : IQ -// Communications and Media Commission -عراق - -// xn--mgbayh7gpa ("al-Ordon", Arabic) : JO -// National Information Technology Center (NITC) -// Royal Scientific Society, Al-Jubeiha -الاردن - -// xn--3e0b707e ("Republic of Korea", Hangul) : KR -한국 - -// xn--80ao21a ("Kaz", Kazakh) : KZ -қаз - -// xn--q7ce6a ("Lao", Lao) : LA -ລາວ - -// xn--fzc2c9e2c ("Lanka", Sinhalese-Sinhala) : LK -// https://nic.lk -ලංකා - -// xn--xkc2al3hye2a ("Ilangai", Tamil) : LK -// https://nic.lk -இலங்கை - -// xn--mgbc0a9azcg ("Morocco/al-Maghrib", Arabic) : MA -المغرب - -// xn--d1alf ("mkd", Macedonian) : MK -// MARnet -мкд - -// xn--l1acc ("mon", Mongolian) : MN -мон - -// xn--mix891f ("Macao", Chinese, Traditional) : MO -// MONIC / HNET Asia (Registry Operator for .mo) -澳門 - -// xn--mix082f ("Macao", Chinese, Simplified) : MO -澳门 - -// xn--mgbx4cd0ab ("Malaysia", Malay) : MY -مليسيا - -// xn--mgb9awbf ("Oman", Arabic) : OM -عمان - -// xn--mgbai9azgqp6j ("Pakistan", Urdu/Arabic) : PK -پاکستان - -// xn--mgbai9a5eva00b ("Pakistan", Urdu/Arabic, variant) : PK -پاكستان - -// xn--ygbi2ammx ("Falasteen", Arabic) : PS -// The Palestinian National Internet Naming Authority (PNINA) -// http://www.pnina.ps -فلسطين - -// xn--90a3ac ("srb", Cyrillic) : RS -// https://www.rnids.rs/en/domains/national-domains -срб -пр.срб -орг.срб -обр.срб -од.срб -упр.срб -ак.срб - -// xn--p1ai ("rf", Russian-Cyrillic) : RU -// https://cctld.ru/files/pdf/docs/en/rules_ru-rf.pdf -// Submitted by George Georgievsky -рф - -// xn--wgbl6a ("Qatar", Arabic) : QA -// http://www.ict.gov.qa/ -قطر - -// xn--mgberp4a5d4ar ("AlSaudiah", Arabic) : SA -// http://www.nic.net.sa/ -السعودية - -// xn--mgberp4a5d4a87g ("AlSaudiah", Arabic, variant) : SA -السعودیة - -// xn--mgbqly7c0a67fbc ("AlSaudiah", Arabic, variant) : SA -السعودیۃ - -// xn--mgbqly7cvafr ("AlSaudiah", Arabic, variant) : SA -السعوديه - -// xn--mgbpl2fh ("sudan", Arabic) : SD -// Operated by .sd registry -سودان - -// xn--yfro4i67o Singapore ("Singapore", Chinese) : SG -新加坡 - -// xn--clchc0ea0b2g2a9gcd ("Singapore", Tamil) : SG -சிங்கப்பூர் - -// xn--ogbpf8fl ("Syria", Arabic) : SY -سورية - -// xn--mgbtf8fl ("Syria", Arabic, variant) : SY -سوريا - -// xn--o3cw4h ("Thai", Thai) : TH -// http://www.thnic.co.th -ไทย -ศึกษา.ไทย -ธุรกิจ.ไทย -รัฐบาล.ไทย -ทหาร.ไทย -เน็ต.ไทย -องค์กร.ไทย - -// xn--pgbs0dh ("Tunisia", Arabic) : TN -// http://nic.tn -تونس - -// xn--kpry57d ("Taiwan", Chinese, Traditional) : TW -// http://www.twnic.net/english/dn/dn_07a.htm -台灣 - -// xn--kprw13d ("Taiwan", Chinese, Simplified) : TW -// http://www.twnic.net/english/dn/dn_07a.htm -台湾 - -// xn--nnx388a ("Taiwan", Chinese, variant) : TW -臺灣 - -// xn--j1amh ("ukr", Cyrillic) : UA -укр - -// xn--mgb2ddes ("AlYemen", Arabic) : YE -اليمن - -// xxx : http://icmregistry.com -xxx - -// ye : http://www.y.net.ye/services/domain_name.htm -ye -com.ye -edu.ye -gov.ye -net.ye -mil.ye -org.ye - -// za : https://www.zadna.org.za/content/page/domain-information/ -ac.za -agric.za -alt.za -co.za -edu.za -gov.za -grondar.za -law.za -mil.za -net.za -ngo.za -nic.za -nis.za -nom.za -org.za -school.za -tm.za -web.za - -// zm : https://zicta.zm/ -// Submitted by registry -zm -ac.zm -biz.zm -co.zm -com.zm -edu.zm -gov.zm -info.zm -mil.zm -net.zm -org.zm -sch.zm - -// zw : https://www.potraz.gov.zw/ -// Confirmed by registry 2017-01-25 -zw -ac.zw -co.zw -gov.zw -mil.zw -org.zw - - -// newGTLDs - -// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2023-03-18T15:13:13Z -// This list is auto-generated, don't edit it manually. -// aaa : 2015-02-26 American Automobile Association, Inc. -aaa - -// aarp : 2015-05-21 AARP -aarp - -// abarth : 2015-07-30 Fiat Chrysler Automobiles N.V. -abarth - -// abb : 2014-10-24 ABB Ltd -abb - -// abbott : 2014-07-24 Abbott Laboratories, Inc. -abbott - -// abbvie : 2015-07-30 AbbVie Inc. -abbvie - -// abc : 2015-07-30 Disney Enterprises, Inc. -abc - -// able : 2015-06-25 Able Inc. -able - -// abogado : 2014-04-24 Registry Services, LLC -abogado - -// abudhabi : 2015-07-30 Abu Dhabi Systems and Information Centre -abudhabi - -// academy : 2013-11-07 Binky Moon, LLC -academy - -// accenture : 2014-08-15 Accenture plc -accenture - -// accountant : 2014-11-20 dot Accountant Limited -accountant - -// accountants : 2014-03-20 Binky Moon, LLC -accountants - -// aco : 2015-01-08 ACO Severin Ahlmann GmbH & Co. KG -aco - -// actor : 2013-12-12 Dog Beach, LLC -actor - -// ads : 2014-12-04 Charleston Road Registry Inc. -ads - -// adult : 2014-10-16 ICM Registry AD LLC -adult - -// aeg : 2015-03-19 Aktiebolaget Electrolux -aeg - -// aetna : 2015-05-21 Aetna Life Insurance Company -aetna - -// afl : 2014-10-02 Australian Football League -afl - -// africa : 2014-03-24 ZA Central Registry NPC trading as Registry.Africa -africa - -// agakhan : 2015-04-23 Fondation Aga Khan (Aga Khan Foundation) -agakhan - -// agency : 2013-11-14 Binky Moon, LLC -agency - -// aig : 2014-12-18 American International Group, Inc. -aig - -// airbus : 2015-07-30 Airbus S.A.S. -airbus - -// airforce : 2014-03-06 Dog Beach, LLC -airforce - -// airtel : 2014-10-24 Bharti Airtel Limited -airtel - -// akdn : 2015-04-23 Fondation Aga Khan (Aga Khan Foundation) -akdn - -// alfaromeo : 2015-07-31 Fiat Chrysler Automobiles N.V. -alfaromeo - -// alibaba : 2015-01-15 Alibaba Group Holding Limited -alibaba - -// alipay : 2015-01-15 Alibaba Group Holding Limited -alipay - -// allfinanz : 2014-07-03 Allfinanz Deutsche Vermögensberatung Aktiengesellschaft -allfinanz - -// allstate : 2015-07-31 Allstate Fire and Casualty Insurance Company -allstate - -// ally : 2015-06-18 Ally Financial Inc. -ally - -// alsace : 2014-07-02 Region Grand Est -alsace - -// alstom : 2015-07-30 ALSTOM -alstom - -// amazon : 2019-12-19 Amazon Registry Services, Inc. -amazon - -// americanexpress : 2015-07-31 American Express Travel Related Services Company, Inc. -americanexpress - -// americanfamily : 2015-07-23 AmFam, Inc. -americanfamily - -// amex : 2015-07-31 American Express Travel Related Services Company, Inc. -amex - -// amfam : 2015-07-23 AmFam, Inc. -amfam - -// amica : 2015-05-28 Amica Mutual Insurance Company -amica - -// amsterdam : 2014-07-24 Gemeente Amsterdam -amsterdam - -// analytics : 2014-12-18 Campus IP LLC -analytics - -// android : 2014-08-07 Charleston Road Registry Inc. -android - -// anquan : 2015-01-08 Beijing Qihu Keji Co., Ltd. -anquan - -// anz : 2015-07-31 Australia and New Zealand Banking Group Limited -anz - -// aol : 2015-09-17 Oath Inc. -aol - -// apartments : 2014-12-11 Binky Moon, LLC -apartments - -// app : 2015-05-14 Charleston Road Registry Inc. -app - -// apple : 2015-05-14 Apple Inc. -apple - -// aquarelle : 2014-07-24 Aquarelle.com -aquarelle - -// arab : 2015-11-12 League of Arab States -arab - -// aramco : 2014-11-20 Aramco Services Company -aramco - -// archi : 2014-02-06 Identity Digital Limited -archi - -// army : 2014-03-06 Dog Beach, LLC -army - -// art : 2016-03-24 UK Creative Ideas Limited -art - -// arte : 2014-12-11 Association Relative à la Télévision Européenne G.E.I.E. -arte - -// asda : 2015-07-31 Wal-Mart Stores, Inc. -asda - -// associates : 2014-03-06 Binky Moon, LLC -associates - -// athleta : 2015-07-30 The Gap, Inc. -athleta - -// attorney : 2014-03-20 Dog Beach, LLC -attorney - -// auction : 2014-03-20 Dog Beach, LLC -auction - -// audi : 2015-05-21 AUDI Aktiengesellschaft -audi - -// audible : 2015-06-25 Amazon Registry Services, Inc. -audible - -// audio : 2014-03-20 XYZ.COM LLC -audio - -// auspost : 2015-08-13 Australian Postal Corporation -auspost - -// author : 2014-12-18 Amazon Registry Services, Inc. -author - -// auto : 2014-11-13 XYZ.COM LLC -auto - -// autos : 2014-01-09 XYZ.COM LLC -autos - -// avianca : 2015-01-08 Avianca Inc. -avianca - -// aws : 2015-06-25 AWS Registry LLC -aws - -// axa : 2013-12-19 AXA Group Operations SAS -axa - -// azure : 2014-12-18 Microsoft Corporation -azure - -// baby : 2015-04-09 XYZ.COM LLC -baby - -// baidu : 2015-01-08 Baidu, Inc. -baidu - -// banamex : 2015-07-30 Citigroup Inc. -banamex - -// bananarepublic : 2015-07-31 The Gap, Inc. -bananarepublic - -// band : 2014-06-12 Dog Beach, LLC -band - -// bank : 2014-09-25 fTLD Registry Services LLC -bank - -// bar : 2013-12-12 Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable -bar - -// barcelona : 2014-07-24 Municipi de Barcelona -barcelona - -// barclaycard : 2014-11-20 Barclays Bank PLC -barclaycard - -// barclays : 2014-11-20 Barclays Bank PLC -barclays - -// barefoot : 2015-06-11 Gallo Vineyards, Inc. -barefoot - -// bargains : 2013-11-14 Binky Moon, LLC -bargains - -// baseball : 2015-10-29 MLB Advanced Media DH, LLC -baseball - -// basketball : 2015-08-20 Fédération Internationale de Basketball (FIBA) -basketball - -// bauhaus : 2014-04-17 Werkhaus GmbH -bauhaus - -// bayern : 2014-01-23 Bayern Connect GmbH -bayern - -// bbc : 2014-12-18 British Broadcasting Corporation -bbc - -// bbt : 2015-07-23 BB&T Corporation -bbt - -// bbva : 2014-10-02 BANCO BILBAO VIZCAYA ARGENTARIA, S.A. -bbva - -// bcg : 2015-04-02 The Boston Consulting Group, Inc. -bcg - -// bcn : 2014-07-24 Municipi de Barcelona -bcn - -// beats : 2015-05-14 Beats Electronics, LLC -beats - -// beauty : 2015-12-03 XYZ.COM LLC -beauty - -// beer : 2014-01-09 Registry Services, LLC -beer - -// bentley : 2014-12-18 Bentley Motors Limited -bentley - -// berlin : 2013-10-31 dotBERLIN GmbH & Co. KG -berlin - -// best : 2013-12-19 BestTLD Pty Ltd -best - -// bestbuy : 2015-07-31 BBY Solutions, Inc. -bestbuy - -// bet : 2015-05-07 Identity Digital Limited -bet - -// bharti : 2014-01-09 Bharti Enterprises (Holding) Private Limited -bharti - -// bible : 2014-06-19 American Bible Society -bible - -// bid : 2013-12-19 dot Bid Limited -bid - -// bike : 2013-08-27 Binky Moon, LLC -bike - -// bing : 2014-12-18 Microsoft Corporation -bing - -// bingo : 2014-12-04 Binky Moon, LLC -bingo - -// bio : 2014-03-06 Identity Digital Limited -bio - -// black : 2014-01-16 Identity Digital Limited -black - -// blackfriday : 2014-01-16 Registry Services, LLC -blackfriday - -// blockbuster : 2015-07-30 Dish DBS Corporation -blockbuster - -// blog : 2015-05-14 Knock Knock WHOIS There, LLC -blog - -// bloomberg : 2014-07-17 Bloomberg IP Holdings LLC -bloomberg - -// blue : 2013-11-07 Identity Digital Limited -blue - -// bms : 2014-10-30 Bristol-Myers Squibb Company -bms - -// bmw : 2014-01-09 Bayerische Motoren Werke Aktiengesellschaft -bmw - -// bnpparibas : 2014-05-29 BNP Paribas -bnpparibas - -// boats : 2014-12-04 XYZ.COM LLC -boats - -// boehringer : 2015-07-09 Boehringer Ingelheim International GmbH -boehringer - -// bofa : 2015-07-31 Bank of America Corporation -bofa - -// bom : 2014-10-16 Núcleo de Informação e Coordenação do Ponto BR - NIC.br -bom - -// bond : 2014-06-05 ShortDot SA -bond - -// boo : 2014-01-30 Charleston Road Registry Inc. -boo - -// book : 2015-08-27 Amazon Registry Services, Inc. -book - -// booking : 2015-07-16 Booking.com B.V. -booking - -// bosch : 2015-06-18 Robert Bosch GMBH -bosch - -// bostik : 2015-05-28 Bostik SA -bostik - -// boston : 2015-12-10 Registry Services, LLC -boston - -// bot : 2014-12-18 Amazon Registry Services, Inc. -bot - -// boutique : 2013-11-14 Binky Moon, LLC -boutique - -// box : 2015-11-12 Intercap Registry Inc. -box - -// bradesco : 2014-12-18 Banco Bradesco S.A. -bradesco - -// bridgestone : 2014-12-18 Bridgestone Corporation -bridgestone - -// broadway : 2014-12-22 Celebrate Broadway, Inc. -broadway - -// broker : 2014-12-11 Dog Beach, LLC -broker - -// brother : 2015-01-29 Brother Industries, Ltd. -brother - -// brussels : 2014-02-06 DNS.be vzw -brussels - -// build : 2013-11-07 Plan Bee LLC -build - -// builders : 2013-11-07 Binky Moon, LLC -builders - -// business : 2013-11-07 Binky Moon, LLC -business - -// buy : 2014-12-18 Amazon Registry Services, Inc. -buy - -// buzz : 2013-10-02 DOTSTRATEGY CO. -buzz - -// bzh : 2014-02-27 Association www.bzh -bzh - -// cab : 2013-10-24 Binky Moon, LLC -cab - -// cafe : 2015-02-11 Binky Moon, LLC -cafe - -// cal : 2014-07-24 Charleston Road Registry Inc. -cal - -// call : 2014-12-18 Amazon Registry Services, Inc. -call - -// calvinklein : 2015-07-30 PVH gTLD Holdings LLC -calvinklein - -// cam : 2016-04-21 Cam Connecting SARL -cam - -// camera : 2013-08-27 Binky Moon, LLC -camera - -// camp : 2013-11-07 Binky Moon, LLC -camp - -// canon : 2014-09-12 Canon Inc. -canon - -// capetown : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry -capetown - -// capital : 2014-03-06 Binky Moon, LLC -capital - -// capitalone : 2015-08-06 Capital One Financial Corporation -capitalone - -// car : 2015-01-22 XYZ.COM LLC -car - -// caravan : 2013-12-12 Caravan International, Inc. -caravan - -// cards : 2013-12-05 Binky Moon, LLC -cards - -// care : 2014-03-06 Binky Moon, LLC -care - -// career : 2013-10-09 dotCareer LLC -career - -// careers : 2013-10-02 Binky Moon, LLC -careers - -// cars : 2014-11-13 XYZ.COM LLC -cars - -// casa : 2013-11-21 Registry Services, LLC -casa - -// case : 2015-09-03 Digity, LLC -case - -// cash : 2014-03-06 Binky Moon, LLC -cash - -// casino : 2014-12-18 Binky Moon, LLC -casino - -// catering : 2013-12-05 Binky Moon, LLC -catering - -// catholic : 2015-10-21 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) -catholic - -// cba : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA -cba - -// cbn : 2014-08-22 The Christian Broadcasting Network, Inc. -cbn - -// cbre : 2015-07-02 CBRE, Inc. -cbre - -// cbs : 2015-08-06 CBS Domains Inc. -cbs - -// center : 2013-11-07 Binky Moon, LLC -center - -// ceo : 2013-11-07 CEOTLD Pty Ltd -ceo - -// cern : 2014-06-05 European Organization for Nuclear Research ("CERN") -cern - -// cfa : 2014-08-28 CFA Institute -cfa - -// cfd : 2014-12-11 ShortDot SA -cfd - -// chanel : 2015-04-09 Chanel International B.V. -chanel - -// channel : 2014-05-08 Charleston Road Registry Inc. -channel - -// charity : 2018-04-11 Public Interest Registry -charity - -// chase : 2015-04-30 JPMorgan Chase Bank, National Association -chase - -// chat : 2014-12-04 Binky Moon, LLC -chat - -// cheap : 2013-11-14 Binky Moon, LLC -cheap - -// chintai : 2015-06-11 CHINTAI Corporation -chintai - -// christmas : 2013-11-21 XYZ.COM LLC -christmas - -// chrome : 2014-07-24 Charleston Road Registry Inc. -chrome - -// church : 2014-02-06 Binky Moon, LLC -church - -// cipriani : 2015-02-19 Hotel Cipriani Srl -cipriani - -// circle : 2014-12-18 Amazon Registry Services, Inc. -circle - -// cisco : 2014-12-22 Cisco Technology, Inc. -cisco - -// citadel : 2015-07-23 Citadel Domain LLC -citadel - -// citi : 2015-07-30 Citigroup Inc. -citi - -// citic : 2014-01-09 CITIC Group Corporation -citic - -// city : 2014-05-29 Binky Moon, LLC -city - -// cityeats : 2014-12-11 Lifestyle Domain Holdings, Inc. -cityeats - -// claims : 2014-03-20 Binky Moon, LLC -claims - -// cleaning : 2013-12-05 Binky Moon, LLC -cleaning - -// click : 2014-06-05 Internet Naming Company LLC -click - -// clinic : 2014-03-20 Binky Moon, LLC -clinic - -// clinique : 2015-10-01 The Estée Lauder Companies Inc. -clinique - -// clothing : 2013-08-27 Binky Moon, LLC -clothing - -// cloud : 2015-04-16 Aruba PEC S.p.A. -cloud - -// club : 2013-11-08 Registry Services, LLC -club - -// clubmed : 2015-06-25 Club Méditerranée S.A. -clubmed - -// coach : 2014-10-09 Binky Moon, LLC -coach - -// codes : 2013-10-31 Binky Moon, LLC -codes - -// coffee : 2013-10-17 Binky Moon, LLC -coffee - -// college : 2014-01-16 XYZ.COM LLC -college - -// cologne : 2014-02-05 dotKoeln GmbH -cologne - -// comcast : 2015-07-23 Comcast IP Holdings I, LLC -comcast - -// commbank : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA -commbank - -// community : 2013-12-05 Binky Moon, LLC -community - -// company : 2013-11-07 Binky Moon, LLC -company - -// compare : 2015-10-08 Registry Services, LLC -compare - -// computer : 2013-10-24 Binky Moon, LLC -computer - -// comsec : 2015-01-08 VeriSign, Inc. -comsec - -// condos : 2013-12-05 Binky Moon, LLC -condos - -// construction : 2013-09-16 Binky Moon, LLC -construction - -// consulting : 2013-12-05 Dog Beach, LLC -consulting - -// contact : 2015-01-08 Dog Beach, LLC -contact - -// contractors : 2013-09-10 Binky Moon, LLC -contractors - -// cooking : 2013-11-21 Registry Services, LLC -cooking - -// cookingchannel : 2015-07-02 Lifestyle Domain Holdings, Inc. -cookingchannel - -// cool : 2013-11-14 Binky Moon, LLC -cool - -// corsica : 2014-09-25 Collectivité de Corse -corsica - -// country : 2013-12-19 Internet Naming Company LLC -country - -// coupon : 2015-02-26 Amazon Registry Services, Inc. -coupon - -// coupons : 2015-03-26 Binky Moon, LLC -coupons - -// courses : 2014-12-04 Registry Services, LLC -courses - -// cpa : 2019-06-10 American Institute of Certified Public Accountants -cpa - -// credit : 2014-03-20 Binky Moon, LLC -credit - -// creditcard : 2014-03-20 Binky Moon, LLC -creditcard - -// creditunion : 2015-01-22 DotCooperation LLC -creditunion - -// cricket : 2014-10-09 dot Cricket Limited -cricket - -// crown : 2014-10-24 Crown Equipment Corporation -crown - -// crs : 2014-04-03 Federated Co-operatives Limited -crs - -// cruise : 2015-12-10 Viking River Cruises (Bermuda) Ltd. -cruise - -// cruises : 2013-12-05 Binky Moon, LLC -cruises - -// cuisinella : 2014-04-03 SCHMIDT GROUPE S.A.S. -cuisinella - -// cymru : 2014-05-08 Nominet UK -cymru - -// cyou : 2015-01-22 ShortDot SA -cyou - -// dabur : 2014-02-06 Dabur India Limited -dabur - -// dad : 2014-01-23 Charleston Road Registry Inc. -dad - -// dance : 2013-10-24 Dog Beach, LLC -dance - -// data : 2016-06-02 Dish DBS Corporation -data - -// date : 2014-11-20 dot Date Limited -date - -// dating : 2013-12-05 Binky Moon, LLC -dating - -// datsun : 2014-03-27 NISSAN MOTOR CO., LTD. -datsun - -// day : 2014-01-30 Charleston Road Registry Inc. -day - -// dclk : 2014-11-20 Charleston Road Registry Inc. -dclk - -// dds : 2015-05-07 Registry Services, LLC -dds - -// deal : 2015-06-25 Amazon Registry Services, Inc. -deal - -// dealer : 2014-12-22 Intercap Registry Inc. -dealer - -// deals : 2014-05-22 Binky Moon, LLC -deals - -// degree : 2014-03-06 Dog Beach, LLC -degree - -// delivery : 2014-09-11 Binky Moon, LLC -delivery - -// dell : 2014-10-24 Dell Inc. -dell - -// deloitte : 2015-07-31 Deloitte Touche Tohmatsu -deloitte - -// delta : 2015-02-19 Delta Air Lines, Inc. -delta - -// democrat : 2013-10-24 Dog Beach, LLC -democrat - -// dental : 2014-03-20 Binky Moon, LLC -dental - -// dentist : 2014-03-20 Dog Beach, LLC -dentist - -// desi : 2013-11-14 Desi Networks LLC -desi - -// design : 2014-11-07 Registry Services, LLC -design - -// dev : 2014-10-16 Charleston Road Registry Inc. -dev - -// dhl : 2015-07-23 Deutsche Post AG -dhl - -// diamonds : 2013-09-22 Binky Moon, LLC -diamonds - -// diet : 2014-06-26 XYZ.COM LLC -diet - -// digital : 2014-03-06 Binky Moon, LLC -digital - -// direct : 2014-04-10 Binky Moon, LLC -direct - -// directory : 2013-09-20 Binky Moon, LLC -directory - -// discount : 2014-03-06 Binky Moon, LLC -discount - -// discover : 2015-07-23 Discover Financial Services -discover - -// dish : 2015-07-30 Dish DBS Corporation -dish - -// diy : 2015-11-05 Lifestyle Domain Holdings, Inc. -diy - -// dnp : 2013-12-13 Dai Nippon Printing Co., Ltd. -dnp - -// docs : 2014-10-16 Charleston Road Registry Inc. -docs - -// doctor : 2016-06-02 Binky Moon, LLC -doctor - -// dog : 2014-12-04 Binky Moon, LLC -dog - -// domains : 2013-10-17 Binky Moon, LLC -domains - -// dot : 2015-05-21 Dish DBS Corporation -dot - -// download : 2014-11-20 dot Support Limited -download - -// drive : 2015-03-05 Charleston Road Registry Inc. -drive - -// dtv : 2015-06-04 Dish DBS Corporation -dtv - -// dubai : 2015-01-01 Dubai Smart Government Department -dubai - -// dunlop : 2015-07-02 The Goodyear Tire & Rubber Company -dunlop - -// dupont : 2015-06-25 DuPont Specialty Products USA, LLC -dupont - -// durban : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry -durban - -// dvag : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG -dvag - -// dvr : 2016-05-26 DISH Technologies L.L.C. -dvr - -// earth : 2014-12-04 Interlink Systems Innovation Institute K.K. -earth - -// eat : 2014-01-23 Charleston Road Registry Inc. -eat - -// eco : 2016-07-08 Big Room Inc. -eco - -// edeka : 2014-12-18 EDEKA Verband kaufmännischer Genossenschaften e.V. -edeka - -// education : 2013-11-07 Binky Moon, LLC -education - -// email : 2013-10-31 Binky Moon, LLC -email - -// emerck : 2014-04-03 Merck KGaA -emerck - -// energy : 2014-09-11 Binky Moon, LLC -energy - -// engineer : 2014-03-06 Dog Beach, LLC -engineer - -// engineering : 2014-03-06 Binky Moon, LLC -engineering - -// enterprises : 2013-09-20 Binky Moon, LLC -enterprises - -// epson : 2014-12-04 Seiko Epson Corporation -epson - -// equipment : 2013-08-27 Binky Moon, LLC -equipment - -// ericsson : 2015-07-09 Telefonaktiebolaget L M Ericsson -ericsson - -// erni : 2014-04-03 ERNI Group Holding AG -erni - -// esq : 2014-05-08 Charleston Road Registry Inc. -esq - -// estate : 2013-08-27 Binky Moon, LLC -estate - -// etisalat : 2015-09-03 Emirates Telecommunications Corporation (trading as Etisalat) -etisalat - -// eurovision : 2014-04-24 European Broadcasting Union (EBU) -eurovision - -// eus : 2013-12-12 Puntueus Fundazioa -eus - -// events : 2013-12-05 Binky Moon, LLC -events - -// exchange : 2014-03-06 Binky Moon, LLC -exchange - -// expert : 2013-11-21 Binky Moon, LLC -expert - -// exposed : 2013-12-05 Binky Moon, LLC -exposed - -// express : 2015-02-11 Binky Moon, LLC -express - -// extraspace : 2015-05-14 Extra Space Storage LLC -extraspace - -// fage : 2014-12-18 Fage International S.A. -fage - -// fail : 2014-03-06 Binky Moon, LLC -fail - -// fairwinds : 2014-11-13 FairWinds Partners, LLC -fairwinds - -// faith : 2014-11-20 dot Faith Limited -faith - -// family : 2015-04-02 Dog Beach, LLC -family - -// fan : 2014-03-06 Dog Beach, LLC -fan - -// fans : 2014-11-07 ZDNS International Limited -fans - -// farm : 2013-11-07 Binky Moon, LLC -farm - -// farmers : 2015-07-09 Farmers Insurance Exchange -farmers - -// fashion : 2014-07-03 Registry Services, LLC -fashion - -// fast : 2014-12-18 Amazon Registry Services, Inc. -fast - -// fedex : 2015-08-06 Federal Express Corporation -fedex - -// feedback : 2013-12-19 Top Level Spectrum, Inc. -feedback - -// ferrari : 2015-07-31 Fiat Chrysler Automobiles N.V. -ferrari - -// ferrero : 2014-12-18 Ferrero Trading Lux S.A. -ferrero - -// fiat : 2015-07-31 Fiat Chrysler Automobiles N.V. -fiat - -// fidelity : 2015-07-30 Fidelity Brokerage Services LLC -fidelity - -// fido : 2015-08-06 Rogers Communications Canada Inc. -fido - -// film : 2015-01-08 Motion Picture Domain Registry Pty Ltd -film - -// final : 2014-10-16 Núcleo de Informação e Coordenação do Ponto BR - NIC.br -final - -// finance : 2014-03-20 Binky Moon, LLC -finance - -// financial : 2014-03-06 Binky Moon, LLC -financial - -// fire : 2015-06-25 Amazon Registry Services, Inc. -fire - -// firestone : 2014-12-18 Bridgestone Licensing Services, Inc -firestone - -// firmdale : 2014-03-27 Firmdale Holdings Limited -firmdale - -// fish : 2013-12-12 Binky Moon, LLC -fish - -// fishing : 2013-11-21 Registry Services, LLC -fishing - -// fit : 2014-11-07 Registry Services, LLC -fit - -// fitness : 2014-03-06 Binky Moon, LLC -fitness - -// flickr : 2015-04-02 Flickr, Inc. -flickr - -// flights : 2013-12-05 Binky Moon, LLC -flights - -// flir : 2015-07-23 FLIR Systems, Inc. -flir - -// florist : 2013-11-07 Binky Moon, LLC -florist - -// flowers : 2014-10-09 XYZ.COM LLC -flowers - -// fly : 2014-05-08 Charleston Road Registry Inc. -fly - -// foo : 2014-01-23 Charleston Road Registry Inc. -foo - -// food : 2016-04-21 Lifestyle Domain Holdings, Inc. -food - -// foodnetwork : 2015-07-02 Lifestyle Domain Holdings, Inc. -foodnetwork - -// football : 2014-12-18 Binky Moon, LLC -football - -// ford : 2014-11-13 Ford Motor Company -ford - -// forex : 2014-12-11 Dog Beach, LLC -forex - -// forsale : 2014-05-22 Dog Beach, LLC -forsale - -// forum : 2015-04-02 Fegistry, LLC -forum - -// foundation : 2013-12-05 Public Interest Registry -foundation - -// fox : 2015-09-11 FOX Registry, LLC -fox - -// free : 2015-12-10 Amazon Registry Services, Inc. -free - -// fresenius : 2015-07-30 Fresenius Immobilien-Verwaltungs-GmbH -fresenius - -// frl : 2014-05-15 FRLregistry B.V. -frl - -// frogans : 2013-12-19 OP3FT -frogans - -// frontdoor : 2015-07-02 Lifestyle Domain Holdings, Inc. -frontdoor - -// frontier : 2015-02-05 Frontier Communications Corporation -frontier - -// ftr : 2015-07-16 Frontier Communications Corporation -ftr - -// fujitsu : 2015-07-30 Fujitsu Limited -fujitsu - -// fun : 2016-01-14 Radix FZC -fun - -// fund : 2014-03-20 Binky Moon, LLC -fund - -// furniture : 2014-03-20 Binky Moon, LLC -furniture - -// futbol : 2013-09-20 Dog Beach, LLC -futbol - -// fyi : 2015-04-02 Binky Moon, LLC -fyi - -// gal : 2013-11-07 Asociación puntoGAL -gal - -// gallery : 2013-09-13 Binky Moon, LLC -gallery - -// gallo : 2015-06-11 Gallo Vineyards, Inc. -gallo - -// gallup : 2015-02-19 Gallup, Inc. -gallup - -// game : 2015-05-28 XYZ.COM LLC -game - -// games : 2015-05-28 Dog Beach, LLC -games - -// gap : 2015-07-31 The Gap, Inc. -gap - -// garden : 2014-06-26 Registry Services, LLC -garden - -// gay : 2019-05-23 Top Level Design, LLC -gay - -// gbiz : 2014-07-17 Charleston Road Registry Inc. -gbiz - -// gdn : 2014-07-31 Joint Stock Company "Navigation-information systems" -gdn - -// gea : 2014-12-04 GEA Group Aktiengesellschaft -gea - -// gent : 2014-01-23 Easyhost BV -gent - -// genting : 2015-03-12 Resorts World Inc Pte. Ltd. -genting - -// george : 2015-07-31 Wal-Mart Stores, Inc. -george - -// ggee : 2014-01-09 GMO Internet, Inc. -ggee - -// gift : 2013-10-17 DotGift, LLC -gift - -// gifts : 2014-07-03 Binky Moon, LLC -gifts - -// gives : 2014-03-06 Public Interest Registry -gives - -// giving : 2014-11-13 Public Interest Registry -giving - -// glass : 2013-11-07 Binky Moon, LLC -glass - -// gle : 2014-07-24 Charleston Road Registry Inc. -gle - -// global : 2014-04-17 Identity Digital Limited -global - -// globo : 2013-12-19 Globo Comunicação e Participações S.A -globo - -// gmail : 2014-05-01 Charleston Road Registry Inc. -gmail - -// gmbh : 2016-01-29 Binky Moon, LLC -gmbh - -// gmo : 2014-01-09 GMO Internet, Inc. -gmo - -// gmx : 2014-04-24 1&1 Mail & Media GmbH -gmx - -// godaddy : 2015-07-23 Go Daddy East, LLC -godaddy - -// gold : 2015-01-22 Binky Moon, LLC -gold - -// goldpoint : 2014-11-20 YODOBASHI CAMERA CO.,LTD. -goldpoint - -// golf : 2014-12-18 Binky Moon, LLC -golf - -// goo : 2014-12-18 NTT Resonant Inc. -goo - -// goodyear : 2015-07-02 The Goodyear Tire & Rubber Company -goodyear - -// goog : 2014-11-20 Charleston Road Registry Inc. -goog - -// google : 2014-07-24 Charleston Road Registry Inc. -google - -// gop : 2014-01-16 Republican State Leadership Committee, Inc. -gop - -// got : 2014-12-18 Amazon Registry Services, Inc. -got - -// grainger : 2015-05-07 Grainger Registry Services, LLC -grainger - -// graphics : 2013-09-13 Binky Moon, LLC -graphics - -// gratis : 2014-03-20 Binky Moon, LLC -gratis - -// green : 2014-05-08 Identity Digital Limited -green - -// gripe : 2014-03-06 Binky Moon, LLC -gripe - -// grocery : 2016-06-16 Wal-Mart Stores, Inc. -grocery - -// group : 2014-08-15 Binky Moon, LLC -group - -// guardian : 2015-07-30 The Guardian Life Insurance Company of America -guardian - -// gucci : 2014-11-13 Guccio Gucci S.p.a. -gucci - -// guge : 2014-08-28 Charleston Road Registry Inc. -guge - -// guide : 2013-09-13 Binky Moon, LLC -guide - -// guitars : 2013-11-14 XYZ.COM LLC -guitars - -// guru : 2013-08-27 Binky Moon, LLC -guru - -// hair : 2015-12-03 XYZ.COM LLC -hair - -// hamburg : 2014-02-20 Hamburg Top-Level-Domain GmbH -hamburg - -// hangout : 2014-11-13 Charleston Road Registry Inc. -hangout - -// haus : 2013-12-05 Dog Beach, LLC -haus - -// hbo : 2015-07-30 HBO Registry Services, Inc. -hbo - -// hdfc : 2015-07-30 HOUSING DEVELOPMENT FINANCE CORPORATION LIMITED -hdfc - -// hdfcbank : 2015-02-12 HDFC Bank Limited -hdfcbank - -// health : 2015-02-11 DotHealth, LLC -health - -// healthcare : 2014-06-12 Binky Moon, LLC -healthcare - -// help : 2014-06-26 Innovation service Limited -help - -// helsinki : 2015-02-05 City of Helsinki -helsinki - -// here : 2014-02-06 Charleston Road Registry Inc. -here - -// hermes : 2014-07-10 HERMES INTERNATIONAL -hermes - -// hgtv : 2015-07-02 Lifestyle Domain Holdings, Inc. -hgtv - -// hiphop : 2014-03-06 Dot Hip Hop, LLC -hiphop - -// hisamitsu : 2015-07-16 Hisamitsu Pharmaceutical Co.,Inc. -hisamitsu - -// hitachi : 2014-10-31 Hitachi, Ltd. -hitachi - -// hiv : 2014-03-13 Internet Naming Company LLC -hiv - -// hkt : 2015-05-14 PCCW-HKT DataCom Services Limited -hkt - -// hockey : 2015-03-19 Binky Moon, LLC -hockey - -// holdings : 2013-08-27 Binky Moon, LLC -holdings - -// holiday : 2013-11-07 Binky Moon, LLC -holiday - -// homedepot : 2015-04-02 Home Depot Product Authority, LLC -homedepot - -// homegoods : 2015-07-16 The TJX Companies, Inc. -homegoods - -// homes : 2014-01-09 XYZ.COM LLC -homes - -// homesense : 2015-07-16 The TJX Companies, Inc. -homesense - -// honda : 2014-12-18 Honda Motor Co., Ltd. -honda - -// horse : 2013-11-21 Registry Services, LLC -horse - -// hospital : 2016-10-20 Binky Moon, LLC -hospital - -// host : 2014-04-17 Radix FZC -host - -// hosting : 2014-05-29 XYZ.COM LLC -hosting - -// hot : 2015-08-27 Amazon Registry Services, Inc. -hot - -// hoteles : 2015-03-05 Travel Reservations SRL -hoteles - -// hotels : 2016-04-07 Booking.com B.V. -hotels - -// hotmail : 2014-12-18 Microsoft Corporation -hotmail - -// house : 2013-11-07 Binky Moon, LLC -house - -// how : 2014-01-23 Charleston Road Registry Inc. -how - -// hsbc : 2014-10-24 HSBC Global Services (UK) Limited -hsbc - -// hughes : 2015-07-30 Hughes Satellite Systems Corporation -hughes - -// hyatt : 2015-07-30 Hyatt GTLD, L.L.C. -hyatt - -// hyundai : 2015-07-09 Hyundai Motor Company -hyundai - -// ibm : 2014-07-31 International Business Machines Corporation -ibm - -// icbc : 2015-02-19 Industrial and Commercial Bank of China Limited -icbc - -// ice : 2014-10-30 IntercontinentalExchange, Inc. -ice - -// icu : 2015-01-08 ShortDot SA -icu - -// ieee : 2015-07-23 IEEE Global LLC -ieee - -// ifm : 2014-01-30 ifm electronic gmbh -ifm - -// ikano : 2015-07-09 Ikano S.A. -ikano - -// imamat : 2015-08-06 Fondation Aga Khan (Aga Khan Foundation) -imamat - -// imdb : 2015-06-25 Amazon Registry Services, Inc. -imdb - -// immo : 2014-07-10 Binky Moon, LLC -immo - -// immobilien : 2013-11-07 Dog Beach, LLC -immobilien - -// inc : 2018-03-10 Intercap Registry Inc. -inc - -// industries : 2013-12-05 Binky Moon, LLC -industries - -// infiniti : 2014-03-27 NISSAN MOTOR CO., LTD. -infiniti - -// ing : 2014-01-23 Charleston Road Registry Inc. -ing - -// ink : 2013-12-05 Top Level Design, LLC -ink - -// institute : 2013-11-07 Binky Moon, LLC -institute - -// insurance : 2015-02-19 fTLD Registry Services LLC -insurance - -// insure : 2014-03-20 Binky Moon, LLC -insure - -// international : 2013-11-07 Binky Moon, LLC -international - -// intuit : 2015-07-30 Intuit Administrative Services, Inc. -intuit - -// investments : 2014-03-20 Binky Moon, LLC -investments - -// ipiranga : 2014-08-28 Ipiranga Produtos de Petroleo S.A. -ipiranga - -// irish : 2014-08-07 Binky Moon, LLC -irish - -// ismaili : 2015-08-06 Fondation Aga Khan (Aga Khan Foundation) -ismaili - -// ist : 2014-08-28 Istanbul Metropolitan Municipality -ist - -// istanbul : 2014-08-28 Istanbul Metropolitan Municipality -istanbul - -// itau : 2014-10-02 Itau Unibanco Holding S.A. -itau - -// itv : 2015-07-09 ITV Services Limited -itv - -// jaguar : 2014-11-13 Jaguar Land Rover Ltd -jaguar - -// java : 2014-06-19 Oracle Corporation -java - -// jcb : 2014-11-20 JCB Co., Ltd. -jcb - -// jeep : 2015-07-30 FCA US LLC. -jeep - -// jetzt : 2014-01-09 Binky Moon, LLC -jetzt - -// jewelry : 2015-03-05 Binky Moon, LLC -jewelry - -// jio : 2015-04-02 Reliance Industries Limited -jio - -// jll : 2015-04-02 Jones Lang LaSalle Incorporated -jll - -// jmp : 2015-03-26 Matrix IP LLC -jmp - -// jnj : 2015-06-18 Johnson & Johnson Services, Inc. -jnj - -// joburg : 2014-03-24 ZA Central Registry NPC trading as ZA Central Registry -joburg - -// jot : 2014-12-18 Amazon Registry Services, Inc. -jot - -// joy : 2014-12-18 Amazon Registry Services, Inc. -joy - -// jpmorgan : 2015-04-30 JPMorgan Chase Bank, National Association -jpmorgan - -// jprs : 2014-09-18 Japan Registry Services Co., Ltd. -jprs - -// juegos : 2014-03-20 Internet Naming Company LLC -juegos - -// juniper : 2015-07-30 JUNIPER NETWORKS, INC. -juniper - -// kaufen : 2013-11-07 Dog Beach, LLC -kaufen - -// kddi : 2014-09-12 KDDI CORPORATION -kddi - -// kerryhotels : 2015-04-30 Kerry Trading Co. Limited -kerryhotels - -// kerrylogistics : 2015-04-09 Kerry Trading Co. Limited -kerrylogistics - -// kerryproperties : 2015-04-09 Kerry Trading Co. Limited -kerryproperties - -// kfh : 2014-12-04 Kuwait Finance House -kfh - -// kia : 2015-07-09 KIA MOTORS CORPORATION -kia - -// kids : 2021-08-13 DotKids Foundation Limited -kids - -// kim : 2013-09-23 Identity Digital Limited -kim - -// kinder : 2014-11-07 Ferrero Trading Lux S.A. -kinder - -// kindle : 2015-06-25 Amazon Registry Services, Inc. -kindle - -// kitchen : 2013-09-20 Binky Moon, LLC -kitchen - -// kiwi : 2013-09-20 DOT KIWI LIMITED -kiwi - -// koeln : 2014-01-09 dotKoeln GmbH -koeln - -// komatsu : 2015-01-08 Komatsu Ltd. -komatsu - -// kosher : 2015-08-20 Kosher Marketing Assets LLC -kosher - -// kpmg : 2015-04-23 KPMG International Cooperative (KPMG International Genossenschaft) -kpmg - -// kpn : 2015-01-08 Koninklijke KPN N.V. -kpn - -// krd : 2013-12-05 KRG Department of Information Technology -krd - -// kred : 2013-12-19 KredTLD Pty Ltd -kred - -// kuokgroup : 2015-04-09 Kerry Trading Co. Limited -kuokgroup - -// kyoto : 2014-11-07 Academic Institution: Kyoto Jyoho Gakuen -kyoto - -// lacaixa : 2014-01-09 Fundación Bancaria Caixa d’Estalvis i Pensions de Barcelona, “la Caixa” -lacaixa - -// lamborghini : 2015-06-04 Automobili Lamborghini S.p.A. -lamborghini - -// lamer : 2015-10-01 The Estée Lauder Companies Inc. -lamer - -// lancaster : 2015-02-12 LANCASTER -lancaster - -// lancia : 2015-07-31 Fiat Chrysler Automobiles N.V. -lancia - -// land : 2013-09-10 Binky Moon, LLC -land - -// landrover : 2014-11-13 Jaguar Land Rover Ltd -landrover - -// lanxess : 2015-07-30 LANXESS Corporation -lanxess - -// lasalle : 2015-04-02 Jones Lang LaSalle Incorporated -lasalle - -// lat : 2014-10-16 XYZ.COM LLC -lat - -// latino : 2015-07-30 Dish DBS Corporation -latino - -// latrobe : 2014-06-16 La Trobe University -latrobe - -// law : 2015-01-22 Registry Services, LLC -law - -// lawyer : 2014-03-20 Dog Beach, LLC -lawyer - -// lds : 2014-03-20 IRI Domain Management, LLC -lds - -// lease : 2014-03-06 Binky Moon, LLC -lease - -// leclerc : 2014-08-07 A.C.D. LEC Association des Centres Distributeurs Edouard Leclerc -leclerc - -// lefrak : 2015-07-16 LeFrak Organization, Inc. -lefrak - -// legal : 2014-10-16 Binky Moon, LLC -legal - -// lego : 2015-07-16 LEGO Juris A/S -lego - -// lexus : 2015-04-23 TOYOTA MOTOR CORPORATION -lexus - -// lgbt : 2014-05-08 Identity Digital Limited -lgbt - -// lidl : 2014-09-18 Schwarz Domains und Services GmbH & Co. KG -lidl - -// life : 2014-02-06 Binky Moon, LLC -life - -// lifeinsurance : 2015-01-15 American Council of Life Insurers -lifeinsurance - -// lifestyle : 2014-12-11 Lifestyle Domain Holdings, Inc. -lifestyle - -// lighting : 2013-08-27 Binky Moon, LLC -lighting - -// like : 2014-12-18 Amazon Registry Services, Inc. -like - -// lilly : 2015-07-31 Eli Lilly and Company -lilly - -// limited : 2014-03-06 Binky Moon, LLC -limited - -// limo : 2013-10-17 Binky Moon, LLC -limo - -// lincoln : 2014-11-13 Ford Motor Company -lincoln - -// link : 2013-11-14 Nova Registry Ltd -link - -// lipsy : 2015-06-25 Lipsy Ltd -lipsy - -// live : 2014-12-04 Dog Beach, LLC -live - -// living : 2015-07-30 Lifestyle Domain Holdings, Inc. -living - -// llc : 2017-12-14 Identity Digital Limited -llc - -// llp : 2019-08-26 Intercap Registry Inc. -llp - -// loan : 2014-11-20 dot Loan Limited -loan - -// loans : 2014-03-20 Binky Moon, LLC -loans - -// locker : 2015-06-04 Dish DBS Corporation -locker - -// locus : 2015-06-25 Locus Analytics LLC -locus - -// lol : 2015-01-30 XYZ.COM LLC -lol - -// london : 2013-11-14 Dot London Domains Limited -london - -// lotte : 2014-11-07 Lotte Holdings Co., Ltd. -lotte - -// lotto : 2014-04-10 Identity Digital Limited -lotto - -// love : 2014-12-22 Merchant Law Group LLP -love - -// lpl : 2015-07-30 LPL Holdings, Inc. -lpl - -// lplfinancial : 2015-07-30 LPL Holdings, Inc. -lplfinancial - -// ltd : 2014-09-25 Binky Moon, LLC -ltd - -// ltda : 2014-04-17 InterNetX, Corp -ltda - -// lundbeck : 2015-08-06 H. Lundbeck A/S -lundbeck - -// luxe : 2014-01-09 Registry Services, LLC -luxe - -// luxury : 2013-10-17 Luxury Partners, LLC -luxury - -// madrid : 2014-05-01 Comunidad de Madrid -madrid - -// maif : 2014-10-02 Mutuelle Assurance Instituteur France (MAIF) -maif - -// maison : 2013-12-05 Binky Moon, LLC -maison - -// makeup : 2015-01-15 XYZ.COM LLC -makeup - -// man : 2014-12-04 MAN SE -man - -// management : 2013-11-07 Binky Moon, LLC -management - -// mango : 2013-10-24 PUNTO FA S.L. -mango - -// map : 2016-06-09 Charleston Road Registry Inc. -map - -// market : 2014-03-06 Dog Beach, LLC -market - -// marketing : 2013-11-07 Binky Moon, LLC -marketing - -// markets : 2014-12-11 Dog Beach, LLC -markets - -// marriott : 2014-10-09 Marriott Worldwide Corporation -marriott - -// marshalls : 2015-07-16 The TJX Companies, Inc. -marshalls - -// maserati : 2015-07-31 Fiat Chrysler Automobiles N.V. -maserati - -// mattel : 2015-08-06 Mattel Sites, Inc. -mattel - -// mba : 2015-04-02 Binky Moon, LLC -mba - -// mckinsey : 2015-07-31 McKinsey Holdings, Inc. -mckinsey - -// med : 2015-08-06 Medistry LLC -med - -// media : 2014-03-06 Binky Moon, LLC -media - -// meet : 2014-01-16 Charleston Road Registry Inc. -meet - -// melbourne : 2014-05-29 The Crown in right of the State of Victoria, represented by its Department of State Development, Business and Innovation -melbourne - -// meme : 2014-01-30 Charleston Road Registry Inc. -meme - -// memorial : 2014-10-16 Dog Beach, LLC -memorial - -// men : 2015-02-26 Exclusive Registry Limited -men - -// menu : 2013-09-11 Dot Menu Registry, LLC -menu - -// merckmsd : 2016-07-14 MSD Registry Holdings, Inc. -merckmsd - -// miami : 2013-12-19 Registry Services, LLC -miami - -// microsoft : 2014-12-18 Microsoft Corporation -microsoft - -// mini : 2014-01-09 Bayerische Motoren Werke Aktiengesellschaft -mini - -// mint : 2015-07-30 Intuit Administrative Services, Inc. -mint - -// mit : 2015-07-02 Massachusetts Institute of Technology -mit - -// mitsubishi : 2015-07-23 Mitsubishi Corporation -mitsubishi - -// mlb : 2015-05-21 MLB Advanced Media DH, LLC -mlb - -// mls : 2015-04-23 The Canadian Real Estate Association -mls - -// mma : 2014-11-07 MMA IARD -mma - -// mobile : 2016-06-02 Dish DBS Corporation -mobile - -// moda : 2013-11-07 Dog Beach, LLC -moda - -// moe : 2013-11-13 Interlink Systems Innovation Institute K.K. -moe - -// moi : 2014-12-18 Amazon Registry Services, Inc. -moi - -// mom : 2015-04-16 XYZ.COM LLC -mom - -// monash : 2013-09-30 Monash University -monash - -// money : 2014-10-16 Binky Moon, LLC -money - -// monster : 2015-09-11 XYZ.COM LLC -monster - -// mormon : 2013-12-05 IRI Domain Management, LLC -mormon - -// mortgage : 2014-03-20 Dog Beach, LLC -mortgage - -// moscow : 2013-12-19 Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) -moscow - -// moto : 2015-06-04 Motorola Trademark Holdings, LLC -moto - -// motorcycles : 2014-01-09 XYZ.COM LLC -motorcycles - -// mov : 2014-01-30 Charleston Road Registry Inc. -mov - -// movie : 2015-02-05 Binky Moon, LLC -movie - -// msd : 2015-07-23 MSD Registry Holdings, Inc. -msd - -// mtn : 2014-12-04 MTN Dubai Limited -mtn - -// mtr : 2015-03-12 MTR Corporation Limited -mtr - -// music : 2021-05-04 DotMusic Limited -music - -// mutual : 2015-04-02 Northwestern Mutual MU TLD Registry, LLC -mutual - -// nab : 2015-08-20 National Australia Bank Limited -nab - -// nagoya : 2013-10-24 GMO Registry, Inc. -nagoya - -// natura : 2015-03-12 NATURA COSMÉTICOS S.A. -natura - -// navy : 2014-03-06 Dog Beach, LLC -navy - -// nba : 2015-07-31 NBA REGISTRY, LLC -nba - -// nec : 2015-01-08 NEC Corporation -nec - -// netbank : 2014-06-26 COMMONWEALTH BANK OF AUSTRALIA -netbank - -// netflix : 2015-06-18 Netflix, Inc. -netflix - -// network : 2013-11-14 Binky Moon, LLC -network - -// neustar : 2013-12-05 NeuStar, Inc. -neustar - -// new : 2014-01-30 Charleston Road Registry Inc. -new - -// news : 2014-12-18 Dog Beach, LLC -news - -// next : 2015-06-18 Next plc -next - -// nextdirect : 2015-06-18 Next plc -nextdirect - -// nexus : 2014-07-24 Charleston Road Registry Inc. -nexus - -// nfl : 2015-07-23 NFL Reg Ops LLC -nfl - -// ngo : 2014-03-06 Public Interest Registry -ngo - -// nhk : 2014-02-13 Japan Broadcasting Corporation (NHK) -nhk - -// nico : 2014-12-04 DWANGO Co., Ltd. -nico - -// nike : 2015-07-23 NIKE, Inc. -nike - -// nikon : 2015-05-21 NIKON CORPORATION -nikon - -// ninja : 2013-11-07 Dog Beach, LLC -ninja - -// nissan : 2014-03-27 NISSAN MOTOR CO., LTD. -nissan - -// nissay : 2015-10-29 Nippon Life Insurance Company -nissay - -// nokia : 2015-01-08 Nokia Corporation -nokia - -// northwesternmutual : 2015-06-18 Northwestern Mutual Registry, LLC -northwesternmutual - -// norton : 2014-12-04 NortonLifeLock Inc. -norton - -// now : 2015-06-25 Amazon Registry Services, Inc. -now - -// nowruz : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. -nowruz - -// nowtv : 2015-05-14 Starbucks (HK) Limited -nowtv - -// nra : 2014-05-22 NRA Holdings Company, INC. -nra - -// nrw : 2013-11-21 Minds + Machines GmbH -nrw - -// ntt : 2014-10-31 NIPPON TELEGRAPH AND TELEPHONE CORPORATION -ntt - -// nyc : 2014-01-23 The City of New York by and through the New York City Department of Information Technology & Telecommunications -nyc - -// obi : 2014-09-25 OBI Group Holding SE & Co. KGaA -obi - -// observer : 2015-04-30 Dog Beach, LLC -observer - -// office : 2015-03-12 Microsoft Corporation -office - -// okinawa : 2013-12-05 BRregistry, Inc. -okinawa - -// olayan : 2015-05-14 Crescent Holding GmbH -olayan - -// olayangroup : 2015-05-14 Crescent Holding GmbH -olayangroup - -// oldnavy : 2015-07-31 The Gap, Inc. -oldnavy - -// ollo : 2015-06-04 Dish DBS Corporation -ollo - -// omega : 2015-01-08 The Swatch Group Ltd -omega - -// one : 2014-11-07 One.com A/S -one - -// ong : 2014-03-06 Public Interest Registry -ong - -// onl : 2013-09-16 iRegistry GmbH -onl - -// online : 2015-01-15 Radix FZC -online - -// ooo : 2014-01-09 INFIBEAM AVENUES LIMITED -ooo - -// open : 2015-07-31 American Express Travel Related Services Company, Inc. -open - -// oracle : 2014-06-19 Oracle Corporation -oracle - -// orange : 2015-03-12 Orange Brand Services Limited -orange - -// organic : 2014-03-27 Identity Digital Limited -organic - -// origins : 2015-10-01 The Estée Lauder Companies Inc. -origins - -// osaka : 2014-09-04 Osaka Registry Co., Ltd. -osaka - -// otsuka : 2013-10-11 Otsuka Holdings Co., Ltd. -otsuka - -// ott : 2015-06-04 Dish DBS Corporation -ott - -// ovh : 2014-01-16 MédiaBC -ovh - -// page : 2014-12-04 Charleston Road Registry Inc. -page - -// panasonic : 2015-07-30 Panasonic Corporation -panasonic - -// paris : 2014-01-30 City of Paris -paris - -// pars : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. -pars - -// partners : 2013-12-05 Binky Moon, LLC -partners - -// parts : 2013-12-05 Binky Moon, LLC -parts - -// party : 2014-09-11 Blue Sky Registry Limited -party - -// passagens : 2015-03-05 Travel Reservations SRL -passagens - -// pay : 2015-08-27 Amazon Registry Services, Inc. -pay - -// pccw : 2015-05-14 PCCW Enterprises Limited -pccw - -// pet : 2015-05-07 Identity Digital Limited -pet - -// pfizer : 2015-09-11 Pfizer Inc. -pfizer - -// pharmacy : 2014-06-19 National Association of Boards of Pharmacy -pharmacy - -// phd : 2016-07-28 Charleston Road Registry Inc. -phd - -// philips : 2014-11-07 Koninklijke Philips N.V. -philips - -// phone : 2016-06-02 Dish DBS Corporation -phone - -// photo : 2013-11-14 Registry Services, LLC -photo - -// photography : 2013-09-20 Binky Moon, LLC -photography - -// photos : 2013-10-17 Binky Moon, LLC -photos - -// physio : 2014-05-01 PhysBiz Pty Ltd -physio - -// pics : 2013-11-14 XYZ.COM LLC -pics - -// pictet : 2014-06-26 Pictet Europe S.A. -pictet - -// pictures : 2014-03-06 Binky Moon, LLC -pictures - -// pid : 2015-01-08 Top Level Spectrum, Inc. -pid - -// pin : 2014-12-18 Amazon Registry Services, Inc. -pin - -// ping : 2015-06-11 Ping Registry Provider, Inc. -ping - -// pink : 2013-10-01 Identity Digital Limited -pink - -// pioneer : 2015-07-16 Pioneer Corporation -pioneer - -// pizza : 2014-06-26 Binky Moon, LLC -pizza - -// place : 2014-04-24 Binky Moon, LLC -place - -// play : 2015-03-05 Charleston Road Registry Inc. -play - -// playstation : 2015-07-02 Sony Interactive Entertainment Inc. -playstation - -// plumbing : 2013-09-10 Binky Moon, LLC -plumbing - -// plus : 2015-02-05 Binky Moon, LLC -plus - -// pnc : 2015-07-02 PNC Domain Co., LLC -pnc - -// pohl : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG -pohl - -// poker : 2014-07-03 Identity Digital Limited -poker - -// politie : 2015-08-20 Politie Nederland -politie - -// porn : 2014-10-16 ICM Registry PN LLC -porn - -// pramerica : 2015-07-30 Prudential Financial, Inc. -pramerica - -// praxi : 2013-12-05 Praxi S.p.A. -praxi - -// press : 2014-04-03 Radix FZC -press - -// prime : 2015-06-25 Amazon Registry Services, Inc. -prime - -// prod : 2014-01-23 Charleston Road Registry Inc. -prod - -// productions : 2013-12-05 Binky Moon, LLC -productions - -// prof : 2014-07-24 Charleston Road Registry Inc. -prof - -// progressive : 2015-07-23 Progressive Casualty Insurance Company -progressive - -// promo : 2014-12-18 Identity Digital Limited -promo - -// properties : 2013-12-05 Binky Moon, LLC -properties - -// property : 2014-05-22 Internet Naming Company LLC -property - -// protection : 2015-04-23 XYZ.COM LLC -protection - -// pru : 2015-07-30 Prudential Financial, Inc. -pru - -// prudential : 2015-07-30 Prudential Financial, Inc. -prudential - -// pub : 2013-12-12 Dog Beach, LLC -pub - -// pwc : 2015-10-29 PricewaterhouseCoopers LLP -pwc - -// qpon : 2013-11-14 dotQPON LLC -qpon - -// quebec : 2013-12-19 PointQuébec Inc -quebec - -// quest : 2015-03-26 XYZ.COM LLC -quest - -// racing : 2014-12-04 Premier Registry Limited -racing - -// radio : 2016-07-21 European Broadcasting Union (EBU) -radio - -// read : 2014-12-18 Amazon Registry Services, Inc. -read - -// realestate : 2015-09-11 dotRealEstate LLC -realestate - -// realtor : 2014-05-29 Real Estate Domains LLC -realtor - -// realty : 2015-03-19 Dog Beach, LLC -realty - -// recipes : 2013-10-17 Binky Moon, LLC -recipes - -// red : 2013-11-07 Identity Digital Limited -red - -// redstone : 2014-10-31 Redstone Haute Couture Co., Ltd. -redstone - -// redumbrella : 2015-03-26 Travelers TLD, LLC -redumbrella - -// rehab : 2014-03-06 Dog Beach, LLC -rehab - -// reise : 2014-03-13 Binky Moon, LLC -reise - -// reisen : 2014-03-06 Binky Moon, LLC -reisen - -// reit : 2014-09-04 National Association of Real Estate Investment Trusts, Inc. -reit - -// reliance : 2015-04-02 Reliance Industries Limited -reliance - -// ren : 2013-12-12 ZDNS International Limited -ren - -// rent : 2014-12-04 XYZ.COM LLC -rent - -// rentals : 2013-12-05 Binky Moon, LLC -rentals - -// repair : 2013-11-07 Binky Moon, LLC -repair - -// report : 2013-12-05 Binky Moon, LLC -report - -// republican : 2014-03-20 Dog Beach, LLC -republican - -// rest : 2013-12-19 Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable -rest - -// restaurant : 2014-07-03 Binky Moon, LLC -restaurant - -// review : 2014-11-20 dot Review Limited -review - -// reviews : 2013-09-13 Dog Beach, LLC -reviews - -// rexroth : 2015-06-18 Robert Bosch GMBH -rexroth - -// rich : 2013-11-21 iRegistry GmbH -rich - -// richardli : 2015-05-14 Pacific Century Asset Management (HK) Limited -richardli - -// ricoh : 2014-11-20 Ricoh Company, Ltd. -ricoh - -// ril : 2015-04-02 Reliance Industries Limited -ril - -// rio : 2014-02-27 Empresa Municipal de Informática SA - IPLANRIO -rio - -// rip : 2014-07-10 Dog Beach, LLC -rip - -// rocher : 2014-12-18 Ferrero Trading Lux S.A. -rocher - -// rocks : 2013-11-14 Dog Beach, LLC -rocks - -// rodeo : 2013-12-19 Registry Services, LLC -rodeo - -// rogers : 2015-08-06 Rogers Communications Canada Inc. -rogers - -// room : 2014-12-18 Amazon Registry Services, Inc. -room - -// rsvp : 2014-05-08 Charleston Road Registry Inc. -rsvp - -// rugby : 2016-12-15 World Rugby Strategic Developments Limited -rugby - -// ruhr : 2013-10-02 dotSaarland GmbH -ruhr - -// run : 2015-03-19 Binky Moon, LLC -run - -// rwe : 2015-04-02 RWE AG -rwe - -// ryukyu : 2014-01-09 BRregistry, Inc. -ryukyu - -// saarland : 2013-12-12 dotSaarland GmbH -saarland - -// safe : 2014-12-18 Amazon Registry Services, Inc. -safe - -// safety : 2015-01-08 Safety Registry Services, LLC. -safety - -// sakura : 2014-12-18 SAKURA Internet Inc. -sakura - -// sale : 2014-10-16 Dog Beach, LLC -sale - -// salon : 2014-12-11 Binky Moon, LLC -salon - -// samsclub : 2015-07-31 Wal-Mart Stores, Inc. -samsclub - -// samsung : 2014-04-03 SAMSUNG SDS CO., LTD -samsung - -// sandvik : 2014-11-13 Sandvik AB -sandvik - -// sandvikcoromant : 2014-11-07 Sandvik AB -sandvikcoromant - -// sanofi : 2014-10-09 Sanofi -sanofi - -// sap : 2014-03-27 SAP AG -sap - -// sarl : 2014-07-03 Binky Moon, LLC -sarl - -// sas : 2015-04-02 Research IP LLC -sas - -// save : 2015-06-25 Amazon Registry Services, Inc. -save - -// saxo : 2014-10-31 Saxo Bank A/S -saxo - -// sbi : 2015-03-12 STATE BANK OF INDIA -sbi - -// sbs : 2014-11-07 ShortDot SA -sbs - -// sca : 2014-03-13 SVENSKA CELLULOSA AKTIEBOLAGET SCA (publ) -sca - -// scb : 2014-02-20 The Siam Commercial Bank Public Company Limited ("SCB") -scb - -// schaeffler : 2015-08-06 Schaeffler Technologies AG & Co. KG -schaeffler - -// schmidt : 2014-04-03 SCHMIDT GROUPE S.A.S. -schmidt - -// scholarships : 2014-04-24 Scholarships.com, LLC -scholarships - -// school : 2014-12-18 Binky Moon, LLC -school - -// schule : 2014-03-06 Binky Moon, LLC -schule - -// schwarz : 2014-09-18 Schwarz Domains und Services GmbH & Co. KG -schwarz - -// science : 2014-09-11 dot Science Limited -science - -// scot : 2014-01-23 Dot Scot Registry Limited -scot - -// search : 2016-06-09 Charleston Road Registry Inc. -search - -// seat : 2014-05-22 SEAT, S.A. (Sociedad Unipersonal) -seat - -// secure : 2015-08-27 Amazon Registry Services, Inc. -secure - -// security : 2015-05-14 XYZ.COM LLC -security - -// seek : 2014-12-04 Seek Limited -seek - -// select : 2015-10-08 Registry Services, LLC -select - -// sener : 2014-10-24 Sener Ingeniería y Sistemas, S.A. -sener - -// services : 2014-02-27 Binky Moon, LLC -services - -// seven : 2015-08-06 Seven West Media Ltd -seven - -// sew : 2014-07-17 SEW-EURODRIVE GmbH & Co KG -sew - -// sex : 2014-11-13 ICM Registry SX LLC -sex - -// sexy : 2013-09-11 Internet Naming Company LLC -sexy - -// sfr : 2015-08-13 Societe Francaise du Radiotelephone - SFR -sfr - -// shangrila : 2015-09-03 Shangri‐La International Hotel Management Limited -shangrila - -// sharp : 2014-05-01 Sharp Corporation -sharp - -// shaw : 2015-04-23 Shaw Cablesystems G.P. -shaw - -// shell : 2015-07-30 Shell Information Technology International Inc -shell - -// shia : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. -shia - -// shiksha : 2013-11-14 Identity Digital Limited -shiksha - -// shoes : 2013-10-02 Binky Moon, LLC -shoes - -// shop : 2016-04-08 GMO Registry, Inc. -shop - -// shopping : 2016-03-31 Binky Moon, LLC -shopping - -// shouji : 2015-01-08 Beijing Qihu Keji Co., Ltd. -shouji - -// show : 2015-03-05 Binky Moon, LLC -show - -// showtime : 2015-08-06 CBS Domains Inc. -showtime - -// silk : 2015-06-25 Amazon Registry Services, Inc. -silk - -// sina : 2015-03-12 Sina Corporation -sina - -// singles : 2013-08-27 Binky Moon, LLC -singles - -// site : 2015-01-15 Radix FZC -site - -// ski : 2015-04-09 Identity Digital Limited -ski - -// skin : 2015-01-15 XYZ.COM LLC -skin - -// sky : 2014-06-19 Sky International AG -sky - -// skype : 2014-12-18 Microsoft Corporation -skype - -// sling : 2015-07-30 DISH Technologies L.L.C. -sling - -// smart : 2015-07-09 Smart Communications, Inc. (SMART) -smart - -// smile : 2014-12-18 Amazon Registry Services, Inc. -smile - -// sncf : 2015-02-19 Société Nationale SNCF -sncf - -// soccer : 2015-03-26 Binky Moon, LLC -soccer - -// social : 2013-11-07 Dog Beach, LLC -social - -// softbank : 2015-07-02 SoftBank Group Corp. -softbank - -// software : 2014-03-20 Dog Beach, LLC -software - -// sohu : 2013-12-19 Sohu.com Limited -sohu - -// solar : 2013-11-07 Binky Moon, LLC -solar - -// solutions : 2013-11-07 Binky Moon, LLC -solutions - -// song : 2015-02-26 Amazon Registry Services, Inc. -song - -// sony : 2015-01-08 Sony Corporation -sony - -// soy : 2014-01-23 Charleston Road Registry Inc. -soy - -// spa : 2019-09-19 Asia Spa and Wellness Promotion Council Limited -spa - -// space : 2014-04-03 Radix FZC -space - -// sport : 2017-11-16 Global Association of International Sports Federations (GAISF) -sport - -// spot : 2015-02-26 Amazon Registry Services, Inc. -spot - -// srl : 2015-05-07 InterNetX, Corp -srl - -// stada : 2014-11-13 STADA Arzneimittel AG -stada - -// staples : 2015-07-30 Staples, Inc. -staples - -// star : 2015-01-08 Star India Private Limited -star - -// statebank : 2015-03-12 STATE BANK OF INDIA -statebank - -// statefarm : 2015-07-30 State Farm Mutual Automobile Insurance Company -statefarm - -// stc : 2014-10-09 Saudi Telecom Company -stc - -// stcgroup : 2014-10-09 Saudi Telecom Company -stcgroup - -// stockholm : 2014-12-18 Stockholms kommun -stockholm - -// storage : 2014-12-22 XYZ.COM LLC -storage - -// store : 2015-04-09 Radix FZC -store - -// stream : 2016-01-08 dot Stream Limited -stream - -// studio : 2015-02-11 Dog Beach, LLC -studio - -// study : 2014-12-11 Registry Services, LLC -study - -// style : 2014-12-04 Binky Moon, LLC -style - -// sucks : 2014-12-22 Vox Populi Registry Ltd. -sucks - -// supplies : 2013-12-19 Binky Moon, LLC -supplies - -// supply : 2013-12-19 Binky Moon, LLC -supply - -// support : 2013-10-24 Binky Moon, LLC -support - -// surf : 2014-01-09 Registry Services, LLC -surf - -// surgery : 2014-03-20 Binky Moon, LLC -surgery - -// suzuki : 2014-02-20 SUZUKI MOTOR CORPORATION -suzuki - -// swatch : 2015-01-08 The Swatch Group Ltd -swatch - -// swiss : 2014-10-16 Swiss Confederation -swiss - -// sydney : 2014-09-18 State of New South Wales, Department of Premier and Cabinet -sydney - -// systems : 2013-11-07 Binky Moon, LLC -systems - -// tab : 2014-12-04 Tabcorp Holdings Limited -tab - -// taipei : 2014-07-10 Taipei City Government -taipei - -// talk : 2015-04-09 Amazon Registry Services, Inc. -talk - -// taobao : 2015-01-15 Alibaba Group Holding Limited -taobao - -// target : 2015-07-31 Target Domain Holdings, LLC -target - -// tatamotors : 2015-03-12 Tata Motors Ltd -tatamotors - -// tatar : 2014-04-24 Limited Liability Company "Coordination Center of Regional Domain of Tatarstan Republic" -tatar - -// tattoo : 2013-08-30 Top Level Design, LLC -tattoo - -// tax : 2014-03-20 Binky Moon, LLC -tax - -// taxi : 2015-03-19 Binky Moon, LLC -taxi - -// tci : 2014-09-12 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. -tci - -// tdk : 2015-06-11 TDK Corporation -tdk - -// team : 2015-03-05 Binky Moon, LLC -team - -// tech : 2015-01-30 Radix FZC -tech - -// technology : 2013-09-13 Binky Moon, LLC -technology - -// temasek : 2014-08-07 Temasek Holdings (Private) Limited -temasek - -// tennis : 2014-12-04 Binky Moon, LLC -tennis - -// teva : 2015-07-02 Teva Pharmaceutical Industries Limited -teva - -// thd : 2015-04-02 Home Depot Product Authority, LLC -thd - -// theater : 2015-03-19 Binky Moon, LLC -theater - -// theatre : 2015-05-07 XYZ.COM LLC -theatre - -// tiaa : 2015-07-23 Teachers Insurance and Annuity Association of America -tiaa - -// tickets : 2015-02-05 XYZ.COM LLC -tickets - -// tienda : 2013-11-14 Binky Moon, LLC -tienda - -// tiffany : 2015-01-30 Tiffany and Company -tiffany - -// tips : 2013-09-20 Binky Moon, LLC -tips - -// tires : 2014-11-07 Binky Moon, LLC -tires - -// tirol : 2014-04-24 punkt Tirol GmbH -tirol - -// tjmaxx : 2015-07-16 The TJX Companies, Inc. -tjmaxx - -// tjx : 2015-07-16 The TJX Companies, Inc. -tjx - -// tkmaxx : 2015-07-16 The TJX Companies, Inc. -tkmaxx - -// tmall : 2015-01-15 Alibaba Group Holding Limited -tmall - -// today : 2013-09-20 Binky Moon, LLC -today - -// tokyo : 2013-11-13 GMO Registry, Inc. -tokyo - -// tools : 2013-11-21 Binky Moon, LLC -tools - -// top : 2014-03-20 .TOP Registry -top - -// toray : 2014-12-18 Toray Industries, Inc. -toray - -// toshiba : 2014-04-10 TOSHIBA Corporation -toshiba - -// total : 2015-08-06 TotalEnergies SE -total - -// tours : 2015-01-22 Binky Moon, LLC -tours - -// town : 2014-03-06 Binky Moon, LLC -town - -// toyota : 2015-04-23 TOYOTA MOTOR CORPORATION -toyota - -// toys : 2014-03-06 Binky Moon, LLC -toys - -// trade : 2014-01-23 Elite Registry Limited -trade - -// trading : 2014-12-11 Dog Beach, LLC -trading - -// training : 2013-11-07 Binky Moon, LLC -training - -// travel : 2015-10-09 Dog Beach, LLC -travel - -// travelchannel : 2015-07-02 Lifestyle Domain Holdings, Inc. -travelchannel - -// travelers : 2015-03-26 Travelers TLD, LLC -travelers - -// travelersinsurance : 2015-03-26 Travelers TLD, LLC -travelersinsurance - -// trust : 2014-10-16 Internet Naming Company LLC -trust - -// trv : 2015-03-26 Travelers TLD, LLC -trv - -// tube : 2015-06-11 Latin American Telecom LLC -tube - -// tui : 2014-07-03 TUI AG -tui - -// tunes : 2015-02-26 Amazon Registry Services, Inc. -tunes - -// tushu : 2014-12-18 Amazon Registry Services, Inc. -tushu - -// tvs : 2015-02-19 T V SUNDRAM IYENGAR & SONS LIMITED -tvs - -// ubank : 2015-08-20 National Australia Bank Limited -ubank - -// ubs : 2014-12-11 UBS AG -ubs - -// unicom : 2015-10-15 China United Network Communications Corporation Limited -unicom - -// university : 2014-03-06 Binky Moon, LLC -university - -// uno : 2013-09-11 Radix FZC -uno - -// uol : 2014-05-01 UBN INTERNET LTDA. -uol - -// ups : 2015-06-25 UPS Market Driver, Inc. -ups - -// vacations : 2013-12-05 Binky Moon, LLC -vacations - -// vana : 2014-12-11 Lifestyle Domain Holdings, Inc. -vana - -// vanguard : 2015-09-03 The Vanguard Group, Inc. -vanguard - -// vegas : 2014-01-16 Dot Vegas, Inc. -vegas - -// ventures : 2013-08-27 Binky Moon, LLC -ventures - -// verisign : 2015-08-13 VeriSign, Inc. -verisign - -// versicherung : 2014-03-20 tldbox GmbH -versicherung - -// vet : 2014-03-06 Dog Beach, LLC -vet - -// viajes : 2013-10-17 Binky Moon, LLC -viajes - -// video : 2014-10-16 Dog Beach, LLC -video - -// vig : 2015-05-14 VIENNA INSURANCE GROUP AG Wiener Versicherung Gruppe -vig - -// viking : 2015-04-02 Viking River Cruises (Bermuda) Ltd. -viking - -// villas : 2013-12-05 Binky Moon, LLC -villas - -// vin : 2015-06-18 Binky Moon, LLC -vin - -// vip : 2015-01-22 Registry Services, LLC -vip - -// virgin : 2014-09-25 Virgin Enterprises Limited -virgin - -// visa : 2015-07-30 Visa Worldwide Pte. Limited -visa - -// vision : 2013-12-05 Binky Moon, LLC -vision - -// viva : 2014-11-07 Saudi Telecom Company -viva - -// vivo : 2015-07-31 Telefonica Brasil S.A. -vivo - -// vlaanderen : 2014-02-06 DNS.be vzw -vlaanderen - -// vodka : 2013-12-19 Registry Services, LLC -vodka - -// volkswagen : 2015-05-14 Volkswagen Group of America Inc. -volkswagen - -// volvo : 2015-11-12 Volvo Holding Sverige Aktiebolag -volvo - -// vote : 2013-11-21 Monolith Registry LLC -vote - -// voting : 2013-11-13 Valuetainment Corp. -voting - -// voto : 2013-11-21 Monolith Registry LLC -voto - -// voyage : 2013-08-27 Binky Moon, LLC -voyage - -// vuelos : 2015-03-05 Travel Reservations SRL -vuelos - -// wales : 2014-05-08 Nominet UK -wales - -// walmart : 2015-07-31 Wal-Mart Stores, Inc. -walmart - -// walter : 2014-11-13 Sandvik AB -walter - -// wang : 2013-10-24 Zodiac Wang Limited -wang - -// wanggou : 2014-12-18 Amazon Registry Services, Inc. -wanggou - -// watch : 2013-11-14 Binky Moon, LLC -watch - -// watches : 2014-12-22 Identity Digital Limited -watches - -// weather : 2015-01-08 International Business Machines Corporation -weather - -// weatherchannel : 2015-03-12 International Business Machines Corporation -weatherchannel - -// webcam : 2014-01-23 dot Webcam Limited -webcam - -// weber : 2015-06-04 Saint-Gobain Weber SA -weber - -// website : 2014-04-03 Radix FZC -website - -// wedding : 2014-04-24 Registry Services, LLC -wedding - -// weibo : 2015-03-05 Sina Corporation -weibo - -// weir : 2015-01-29 Weir Group IP Limited -weir - -// whoswho : 2014-02-20 Who's Who Registry -whoswho - -// wien : 2013-10-28 punkt.wien GmbH -wien - -// wiki : 2013-11-07 Top Level Design, LLC -wiki - -// williamhill : 2014-03-13 William Hill Organization Limited -williamhill - -// win : 2014-11-20 First Registry Limited -win - -// windows : 2014-12-18 Microsoft Corporation -windows - -// wine : 2015-06-18 Binky Moon, LLC -wine - -// winners : 2015-07-16 The TJX Companies, Inc. -winners - -// wme : 2014-02-13 William Morris Endeavor Entertainment, LLC -wme - -// wolterskluwer : 2015-08-06 Wolters Kluwer N.V. -wolterskluwer - -// woodside : 2015-07-09 Woodside Petroleum Limited -woodside - -// work : 2013-12-19 Registry Services, LLC -work - -// works : 2013-11-14 Binky Moon, LLC -works - -// world : 2014-06-12 Binky Moon, LLC -world - -// wow : 2015-10-08 Amazon Registry Services, Inc. -wow - -// wtc : 2013-12-19 World Trade Centers Association, Inc. -wtc - -// wtf : 2014-03-06 Binky Moon, LLC -wtf - -// xbox : 2014-12-18 Microsoft Corporation -xbox - -// xerox : 2014-10-24 Xerox DNHC LLC -xerox - -// xfinity : 2015-07-09 Comcast IP Holdings I, LLC -xfinity - -// xihuan : 2015-01-08 Beijing Qihu Keji Co., Ltd. -xihuan - -// xin : 2014-12-11 Elegant Leader Limited -xin - -// xn--11b4c3d : 2015-01-15 VeriSign Sarl -कॉम - -// xn--1ck2e1b : 2015-02-26 Amazon Registry Services, Inc. -セール - -// xn--1qqw23a : 2014-01-09 Guangzhou YU Wei Information Technology Co., Ltd. -佛山 - -// xn--30rr7y : 2014-06-12 Excellent First Limited -慈善 - -// xn--3bst00m : 2013-09-13 Eagle Horizon Limited -集团 - -// xn--3ds443g : 2013-09-08 TLD REGISTRY LIMITED OY -在线 - -// xn--3pxu8k : 2015-01-15 VeriSign Sarl -点看 - -// xn--42c2d9a : 2015-01-15 VeriSign Sarl -คอม - -// xn--45q11c : 2013-11-21 Zodiac Gemini Ltd -八卦 - -// xn--4gbrim : 2013-10-04 Helium TLDs Ltd -موقع - -// xn--55qw42g : 2013-11-08 China Organizational Name Administration Center -公益 - -// xn--55qx5d : 2013-11-14 China Internet Network Information Center (CNNIC) -公司 - -// xn--5su34j936bgsg : 2015-09-03 Shangri‐La International Hotel Management Limited -香格里拉 - -// xn--5tzm5g : 2014-12-22 Global Website TLD Asia Limited -网站 - -// xn--6frz82g : 2013-09-23 Identity Digital Limited -移动 - -// xn--6qq986b3xl : 2013-09-13 Tycoon Treasure Limited -我爱你 - -// xn--80adxhks : 2013-12-19 Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) -москва - -// xn--80aqecdr1a : 2015-10-21 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) -католик - -// xn--80asehdb : 2013-07-14 CORE Association -онлайн - -// xn--80aswg : 2013-07-14 CORE Association -сайт - -// xn--8y0a063a : 2015-03-26 China United Network Communications Corporation Limited -联通 - -// xn--9dbq2a : 2015-01-15 VeriSign Sarl -קום - -// xn--9et52u : 2014-06-12 RISE VICTORY LIMITED -时尚 - -// xn--9krt00a : 2015-03-12 Sina Corporation -微博 - -// xn--b4w605ferd : 2014-08-07 Temasek Holdings (Private) Limited -淡马锡 - -// xn--bck1b9a5dre4c : 2015-02-26 Amazon Registry Services, Inc. -ファッション - -// xn--c1avg : 2013-11-14 Public Interest Registry -орг - -// xn--c2br7g : 2015-01-15 VeriSign Sarl -नेट - -// xn--cck2b3b : 2015-02-26 Amazon Registry Services, Inc. -ストア - -// xn--cckwcxetd : 2019-12-19 Amazon Registry Services, Inc. -アマゾン - -// xn--cg4bki : 2013-09-27 SAMSUNG SDS CO., LTD -삼성 - -// xn--czr694b : 2014-01-16 Internet DotTrademark Organisation Limited -商标 - -// xn--czrs0t : 2013-12-19 Binky Moon, LLC -商店 - -// xn--czru2d : 2013-11-21 Zodiac Aquarius Limited -商城 - -// xn--d1acj3b : 2013-11-20 The Foundation for Network Initiatives “The Smart Internet” -дети - -// xn--eckvdtc9d : 2014-12-18 Amazon Registry Services, Inc. -ポイント - -// xn--efvy88h : 2014-08-22 Guangzhou YU Wei Information Technology Co., Ltd. -新闻 - -// xn--fct429k : 2015-04-09 Amazon Registry Services, Inc. -家電 - -// xn--fhbei : 2015-01-15 VeriSign Sarl -كوم - -// xn--fiq228c5hs : 2013-09-08 TLD REGISTRY LIMITED OY -中文网 - -// xn--fiq64b : 2013-10-14 CITIC Group Corporation -中信 - -// xn--fjq720a : 2014-05-22 Binky Moon, LLC -娱乐 - -// xn--flw351e : 2014-07-31 Charleston Road Registry Inc. -谷歌 - -// xn--fzys8d69uvgm : 2015-05-14 PCCW Enterprises Limited -電訊盈科 - -// xn--g2xx48c : 2015-01-30 Nawang Heli(Xiamen) Network Service Co., LTD. -购物 - -// xn--gckr3f0f : 2015-02-26 Amazon Registry Services, Inc. -クラウド - -// xn--gk3at1e : 2015-10-08 Amazon Registry Services, Inc. -通販 - -// xn--hxt814e : 2014-05-15 Zodiac Taurus Limited -网店 - -// xn--i1b6b1a6a2e : 2013-11-14 Public Interest Registry -संगठन - -// xn--imr513n : 2014-12-11 Internet DotTrademark Organisation Limited -餐厅 - -// xn--io0a7i : 2013-11-14 China Internet Network Information Center (CNNIC) -网络 - -// xn--j1aef : 2015-01-15 VeriSign Sarl -ком - -// xn--jlq480n2rg : 2019-12-19 Amazon Registry Services, Inc. -亚马逊 - -// xn--jvr189m : 2015-02-26 Amazon Registry Services, Inc. -食品 - -// xn--kcrx77d1x4a : 2014-11-07 Koninklijke Philips N.V. -飞利浦 - -// xn--kput3i : 2014-02-13 Beijing RITT-Net Technology Development Co., Ltd -手机 - -// xn--mgba3a3ejt : 2014-11-20 Aramco Services Company -ارامكو - -// xn--mgba7c0bbn0a : 2015-05-14 Crescent Holding GmbH -العليان - -// xn--mgbaakc7dvf : 2015-09-03 Emirates Telecommunications Corporation (trading as Etisalat) -اتصالات - -// xn--mgbab2bd : 2013-10-31 CORE Association -بازار - -// xn--mgbca7dzdo : 2015-07-30 Abu Dhabi Systems and Information Centre -ابوظبي - -// xn--mgbi4ecexp : 2015-10-21 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) -كاثوليك - -// xn--mgbt3dhd : 2014-09-04 Asia Green IT System Bilgisayar San. ve Tic. Ltd. Sti. -همراه - -// xn--mk1bu44c : 2015-01-15 VeriSign Sarl -닷컴 - -// xn--mxtq1m : 2014-03-06 Net-Chinese Co., Ltd. -政府 - -// xn--ngbc5azd : 2013-07-13 International Domain Registry Pty. Ltd. -شبكة - -// xn--ngbe9e0a : 2014-12-04 Kuwait Finance House -بيتك - -// xn--ngbrx : 2015-11-12 League of Arab States -عرب - -// xn--nqv7f : 2013-11-14 Public Interest Registry -机构 - -// xn--nqv7fs00ema : 2013-11-14 Public Interest Registry -组织机构 - -// xn--nyqy26a : 2014-11-07 Stable Tone Limited -健康 - -// xn--otu796d : 2017-08-06 Jiang Yu Liang Cai Technology Company Limited -招聘 - -// xn--p1acf : 2013-12-12 Rusnames Limited -рус - -// xn--pssy2u : 2015-01-15 VeriSign Sarl -大拿 - -// xn--q9jyb4c : 2013-09-17 Charleston Road Registry Inc. -みんな - -// xn--qcka1pmc : 2014-07-31 Charleston Road Registry Inc. -グーグル - -// xn--rhqv96g : 2013-09-11 Stable Tone Limited -世界 - -// xn--rovu88b : 2015-02-26 Amazon Registry Services, Inc. -書籍 - -// xn--ses554g : 2014-01-16 KNET Co., Ltd. -网址 - -// xn--t60b56a : 2015-01-15 VeriSign Sarl -닷넷 - -// xn--tckwe : 2015-01-15 VeriSign Sarl -コム - -// xn--tiq49xqyj : 2015-10-21 Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) -天主教 - -// xn--unup4y : 2013-07-14 Binky Moon, LLC -游戏 - -// xn--vermgensberater-ctb : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG -vermögensberater - -// xn--vermgensberatung-pwb : 2014-06-23 Deutsche Vermögensberatung Aktiengesellschaft DVAG -vermögensberatung - -// xn--vhquv : 2013-08-27 Binky Moon, LLC -企业 - -// xn--vuq861b : 2014-10-16 Beijing Tele-info Network Technology Co., Ltd. -信息 - -// xn--w4r85el8fhu5dnra : 2015-04-30 Kerry Trading Co. Limited -嘉里大酒店 - -// xn--w4rs40l : 2015-07-30 Kerry Trading Co. Limited -嘉里 - -// xn--xhq521b : 2013-11-14 Guangzhou YU Wei Information Technology Co., Ltd. -广东 - -// xn--zfr164b : 2013-11-08 China Organizational Name Administration Center -政务 - -// xyz : 2013-12-05 XYZ.COM LLC -xyz - -// yachts : 2014-01-09 XYZ.COM LLC -yachts - -// yahoo : 2015-04-02 Oath Inc. -yahoo - -// yamaxun : 2014-12-18 Amazon Registry Services, Inc. -yamaxun - -// yandex : 2014-04-10 Yandex Europe B.V. -yandex - -// yodobashi : 2014-11-20 YODOBASHI CAMERA CO.,LTD. -yodobashi - -// yoga : 2014-05-29 Registry Services, LLC -yoga - -// yokohama : 2013-12-12 GMO Registry, Inc. -yokohama - -// you : 2015-04-09 Amazon Registry Services, Inc. -you - -// youtube : 2014-05-01 Charleston Road Registry Inc. -youtube - -// yun : 2015-01-08 Beijing Qihu Keji Co., Ltd. -yun - -// zappos : 2015-06-25 Amazon Registry Services, Inc. -zappos - -// zara : 2014-11-07 Industria de Diseño Textil, S.A. (INDITEX, S.A.) -zara - -// zero : 2014-12-18 Amazon Registry Services, Inc. -zero - -// zip : 2014-05-08 Charleston Road Registry Inc. -zip - -// zone : 2013-11-14 Binky Moon, LLC -zone - -// zuerich : 2014-11-07 Kanton Zürich (Canton of Zurich) -zuerich - - -// ===END ICANN DOMAINS=== -// ===BEGIN PRIVATE DOMAINS=== -// (Note: these are in alphabetical order by company name) - -// 1GB LLC : https://www.1gb.ua/ -// Submitted by 1GB LLC -cc.ua -inf.ua -ltd.ua - -// 611coin : https://611project.org/ -611.to - -// Aaron Marais' Gitlab pages: https://lab.aaronleem.co.za -// Submitted by Aaron Marais -graphox.us - -// accesso Technology Group, plc. : https://accesso.com/ -// Submitted by accesso Team -*.devcdnaccesso.com - -// Acorn Labs : https://acorn.io -// Submitted by Craig Jellick -*.on-acorn.io - -// ActiveTrail: https://www.activetrail.biz/ -// Submitted by Ofer Kalaora -activetrail.biz - -// Adobe : https://www.adobe.com/ -// Submitted by Ian Boston and Lars Trieloff -adobeaemcloud.com -*.dev.adobeaemcloud.com -hlx.live -adobeaemcloud.net -hlx.page -hlx3.page - -// Adobe Developer Platform : https://developer.adobe.com -// Submitted by Jesse MacFadyen -adobeio-static.net -adobeioruntime.net - -// Agnat sp. z o.o. : https://domena.pl -// Submitted by Przemyslaw Plewa -beep.pl - -// Airkit : https://www.airkit.com/ -// Submitted by Grant Cooksey -airkitapps.com -airkitapps-au.com -airkitapps.eu - -// Aiven: https://aiven.io/ -// Submitted by Etienne Stalmans -aivencloud.com - -// Akamai : https://www.akamai.com/ -// Submitted by Akamai Team -akadns.net -akamai.net -akamai-staging.net -akamaiedge.net -akamaiedge-staging.net -akamaihd.net -akamaihd-staging.net -akamaiorigin.net -akamaiorigin-staging.net -akamaized.net -akamaized-staging.net -edgekey.net -edgekey-staging.net -edgesuite.net -edgesuite-staging.net - -// alboto.ca : http://alboto.ca -// Submitted by Anton Avramov -barsy.ca - -// Alces Software Ltd : http://alces-software.com -// Submitted by Mark J. Titorenko -*.compute.estate -*.alces.network - -// all-inkl.com : https://all-inkl.com -// Submitted by Werner Kaltofen -kasserver.com - -// Altervista: https://www.altervista.org -// Submitted by Carlo Cannas -altervista.org - -// alwaysdata : https://www.alwaysdata.com -// Submitted by Cyril -alwaysdata.net - -// Amaze Software : https://amaze.co -// Submitted by Domain Admin -myamaze.net - -// Amazon : https://www.amazon.com/ -// Submitted by AWS Security -// Subsections of Amazon/subsidiaries will appear until "concludes" tag - -// Amazon CloudFront -// Submitted by Donavan Miller -// Reference: 54144616-fd49-4435-8535-19c6a601bdb3 -cloudfront.net - -// Amazon EC2 -// Submitted by Luke Wells -// Reference: 4c38fa71-58ac-4768-99e5-689c1767e537 -*.compute.amazonaws.com -*.compute-1.amazonaws.com -*.compute.amazonaws.com.cn -us-east-1.amazonaws.com - -// Amazon S3 -// Submitted by Luke Wells -// Reference: d068bd97-f0a9-4838-a6d8-954b622ef4ae -s3.cn-north-1.amazonaws.com.cn -s3.dualstack.ap-northeast-1.amazonaws.com -s3.dualstack.ap-northeast-2.amazonaws.com -s3.ap-northeast-2.amazonaws.com -s3-website.ap-northeast-2.amazonaws.com -s3.dualstack.ap-south-1.amazonaws.com -s3.ap-south-1.amazonaws.com -s3-website.ap-south-1.amazonaws.com -s3.dualstack.ap-southeast-1.amazonaws.com -s3.dualstack.ap-southeast-2.amazonaws.com -s3.dualstack.ca-central-1.amazonaws.com -s3.ca-central-1.amazonaws.com -s3-website.ca-central-1.amazonaws.com -s3.dualstack.eu-central-1.amazonaws.com -s3.eu-central-1.amazonaws.com -s3-website.eu-central-1.amazonaws.com -s3.dualstack.eu-west-1.amazonaws.com -s3.dualstack.eu-west-2.amazonaws.com -s3.eu-west-2.amazonaws.com -s3-website.eu-west-2.amazonaws.com -s3.dualstack.eu-west-3.amazonaws.com -s3.eu-west-3.amazonaws.com -s3-website.eu-west-3.amazonaws.com -s3.amazonaws.com -s3-ap-northeast-1.amazonaws.com -s3-ap-northeast-2.amazonaws.com -s3-ap-south-1.amazonaws.com -s3-ap-southeast-1.amazonaws.com -s3-ap-southeast-2.amazonaws.com -s3-ca-central-1.amazonaws.com -s3-eu-central-1.amazonaws.com -s3-eu-west-1.amazonaws.com -s3-eu-west-2.amazonaws.com -s3-eu-west-3.amazonaws.com -s3-external-1.amazonaws.com -s3-fips-us-gov-west-1.amazonaws.com -s3-sa-east-1.amazonaws.com -s3-us-east-2.amazonaws.com -s3-us-gov-west-1.amazonaws.com -s3-us-west-1.amazonaws.com -s3-us-west-2.amazonaws.com -s3-website-ap-northeast-1.amazonaws.com -s3-website-ap-southeast-1.amazonaws.com -s3-website-ap-southeast-2.amazonaws.com -s3-website-eu-west-1.amazonaws.com -s3-website-sa-east-1.amazonaws.com -s3-website-us-east-1.amazonaws.com -s3-website-us-west-1.amazonaws.com -s3-website-us-west-2.amazonaws.com -s3.dualstack.sa-east-1.amazonaws.com -s3.dualstack.us-east-1.amazonaws.com -s3.dualstack.us-east-2.amazonaws.com -s3.us-east-2.amazonaws.com -s3-website.us-east-2.amazonaws.com - -// AWS Cloud9 -// Submitted by: AWS Security -// Reference: 2b6dfa9a-3a7f-4367-b2e7-0321e77c0d59 -vfs.cloud9.af-south-1.amazonaws.com -webview-assets.cloud9.af-south-1.amazonaws.com -vfs.cloud9.ap-east-1.amazonaws.com -webview-assets.cloud9.ap-east-1.amazonaws.com -vfs.cloud9.ap-northeast-1.amazonaws.com -webview-assets.cloud9.ap-northeast-1.amazonaws.com -vfs.cloud9.ap-northeast-2.amazonaws.com -webview-assets.cloud9.ap-northeast-2.amazonaws.com -vfs.cloud9.ap-northeast-3.amazonaws.com -webview-assets.cloud9.ap-northeast-3.amazonaws.com -vfs.cloud9.ap-south-1.amazonaws.com -webview-assets.cloud9.ap-south-1.amazonaws.com -vfs.cloud9.ap-southeast-1.amazonaws.com -webview-assets.cloud9.ap-southeast-1.amazonaws.com -vfs.cloud9.ap-southeast-2.amazonaws.com -webview-assets.cloud9.ap-southeast-2.amazonaws.com -vfs.cloud9.ca-central-1.amazonaws.com -webview-assets.cloud9.ca-central-1.amazonaws.com -vfs.cloud9.eu-central-1.amazonaws.com -webview-assets.cloud9.eu-central-1.amazonaws.com -vfs.cloud9.eu-north-1.amazonaws.com -webview-assets.cloud9.eu-north-1.amazonaws.com -vfs.cloud9.eu-south-1.amazonaws.com -webview-assets.cloud9.eu-south-1.amazonaws.com -vfs.cloud9.eu-west-1.amazonaws.com -webview-assets.cloud9.eu-west-1.amazonaws.com -vfs.cloud9.eu-west-2.amazonaws.com -webview-assets.cloud9.eu-west-2.amazonaws.com -vfs.cloud9.eu-west-3.amazonaws.com -webview-assets.cloud9.eu-west-3.amazonaws.com -vfs.cloud9.me-south-1.amazonaws.com -webview-assets.cloud9.me-south-1.amazonaws.com -vfs.cloud9.sa-east-1.amazonaws.com -webview-assets.cloud9.sa-east-1.amazonaws.com -vfs.cloud9.us-east-1.amazonaws.com -webview-assets.cloud9.us-east-1.amazonaws.com -vfs.cloud9.us-east-2.amazonaws.com -webview-assets.cloud9.us-east-2.amazonaws.com -vfs.cloud9.us-west-1.amazonaws.com -webview-assets.cloud9.us-west-1.amazonaws.com -vfs.cloud9.us-west-2.amazonaws.com -webview-assets.cloud9.us-west-2.amazonaws.com - -// AWS Elastic Beanstalk -// Submitted by Luke Wells -// Reference: aa202394-43a0-4857-b245-8db04549137e -cn-north-1.eb.amazonaws.com.cn -cn-northwest-1.eb.amazonaws.com.cn -elasticbeanstalk.com -ap-northeast-1.elasticbeanstalk.com -ap-northeast-2.elasticbeanstalk.com -ap-northeast-3.elasticbeanstalk.com -ap-south-1.elasticbeanstalk.com -ap-southeast-1.elasticbeanstalk.com -ap-southeast-2.elasticbeanstalk.com -ca-central-1.elasticbeanstalk.com -eu-central-1.elasticbeanstalk.com -eu-west-1.elasticbeanstalk.com -eu-west-2.elasticbeanstalk.com -eu-west-3.elasticbeanstalk.com -sa-east-1.elasticbeanstalk.com -us-east-1.elasticbeanstalk.com -us-east-2.elasticbeanstalk.com -us-gov-west-1.elasticbeanstalk.com -us-west-1.elasticbeanstalk.com -us-west-2.elasticbeanstalk.com - -// (AWS) Elastic Load Balancing -// Submitted by Luke Wells -// Reference: 12a3d528-1bac-4433-a359-a395867ffed2 -*.elb.amazonaws.com.cn -*.elb.amazonaws.com - -// AWS Global Accelerator -// Submitted by Daniel Massaguer -// Reference: d916759d-a08b-4241-b536-4db887383a6a -awsglobalaccelerator.com - -// eero -// Submitted by Yue Kang -// Reference: 264afe70-f62c-4c02-8ab9-b5281ed24461 -eero.online -eero-stage.online - -// concludes Amazon - -// Amune : https://amune.org/ -// Submitted by Team Amune -t3l3p0rt.net -tele.amune.org - -// Apigee : https://apigee.com/ -// Submitted by Apigee Security Team -apigee.io - -// Apphud : https://apphud.com -// Submitted by Alexander Selivanov -siiites.com - -// Appspace : https://www.appspace.com -// Submitted by Appspace Security Team -appspacehosted.com -appspaceusercontent.com - -// Appudo UG (haftungsbeschränkt) : https://www.appudo.com -// Submitted by Alexander Hochbaum -appudo.net - -// Aptible : https://www.aptible.com/ -// Submitted by Thomas Orozco -on-aptible.com - -// ASEINet : https://www.aseinet.com/ -// Submitted by Asei SEKIGUCHI -user.aseinet.ne.jp -gv.vc -d.gv.vc - -// Asociación Amigos de la Informática "Euskalamiga" : http://encounter.eus/ -// Submitted by Hector Martin -user.party.eus - -// Association potager.org : https://potager.org/ -// Submitted by Lunar -pimienta.org -poivron.org -potager.org -sweetpepper.org - -// ASUSTOR Inc. : http://www.asustor.com -// Submitted by Vincent Tseng -myasustor.com - -// Atlassian : https://atlassian.com -// Submitted by Sam Smyth -cdn.prod.atlassian-dev.net - -// Authentick UG (haftungsbeschränkt) : https://authentick.net -// Submitted by Lukas Reschke -translated.page - -// Autocode : https://autocode.com -// Submitted by Jacob Lee -autocode.dev - -// AVM : https://avm.de -// Submitted by Andreas Weise -myfritz.net - -// AVStack Pte. Ltd. : https://avstack.io -// Submitted by Jasper Hugo -onavstack.net - -// AW AdvisorWebsites.com Software Inc : https://advisorwebsites.com -// Submitted by James Kennedy -*.awdev.ca -*.advisor.ws - -// AZ.pl sp. z.o.o: https://az.pl -// Submitted by Krzysztof Wolski -ecommerce-shop.pl - -// b-data GmbH : https://www.b-data.io -// Submitted by Olivier Benz -b-data.io - -// backplane : https://www.backplane.io -// Submitted by Anthony Voutas -backplaneapp.io - -// Balena : https://www.balena.io -// Submitted by Petros Angelatos -balena-devices.com - -// University of Banja Luka : https://unibl.org -// Domains for Republic of Srpska administrative entity. -// Submitted by Marko Ivanovic -rs.ba - -// Banzai Cloud -// Submitted by Janos Matyas -*.banzai.cloud -app.banzaicloud.io -*.backyards.banzaicloud.io - -// BASE, Inc. : https://binc.jp -// Submitted by Yuya NAGASAWA -base.ec -official.ec -buyshop.jp -fashionstore.jp -handcrafted.jp -kawaiishop.jp -supersale.jp -theshop.jp -shopselect.net -base.shop - -// BeagleBoard.org Foundation : https://beagleboard.org -// Submitted by Jason Kridner -beagleboard.io - -// Beget Ltd -// Submitted by Lev Nekrasov -*.beget.app - -// BetaInABox -// Submitted by Adrian -betainabox.com - -// BinaryLane : http://www.binarylane.com -// Submitted by Nathan O'Sullivan -bnr.la - -// Bitbucket : http://bitbucket.org -// Submitted by Andy Ortlieb -bitbucket.io - -// Blackbaud, Inc. : https://www.blackbaud.com -// Submitted by Paul Crowder -blackbaudcdn.net - -// Blatech : http://www.blatech.net -// Submitted by Luke Bratch -of.je - -// Blue Bite, LLC : https://bluebite.com -// Submitted by Joshua Weiss -bluebite.io - -// Boomla : https://boomla.com -// Submitted by Tibor Halter -boomla.net - -// Boutir : https://www.boutir.com -// Submitted by Eric Ng Ka Ka -boutir.com - -// Boxfuse : https://boxfuse.com -// Submitted by Axel Fontaine -boxfuse.io - -// bplaced : https://www.bplaced.net/ -// Submitted by Miroslav Bozic -square7.ch -bplaced.com -bplaced.de -square7.de -bplaced.net -square7.net - -// Brendly : https://brendly.rs -// Submitted by Dusan Radovanovic -shop.brendly.rs - -// BrowserSafetyMark -// Submitted by Dave Tharp -browsersafetymark.io - -// Bytemark Hosting : https://www.bytemark.co.uk -// Submitted by Paul Cammish -uk0.bigv.io -dh.bytemark.co.uk -vm.bytemark.co.uk - -// Caf.js Labs LLC : https://www.cafjs.com -// Submitted by Antonio Lain -cafjs.com - -// callidomus : https://www.callidomus.com/ -// Submitted by Marcus Popp -mycd.eu - -// Canva Pty Ltd : https://canva.com/ -// Submitted by Joel Aquilina -canva-apps.cn -canva-apps.com - -// Carrd : https://carrd.co -// Submitted by AJ -drr.ac -uwu.ai -carrd.co -crd.co -ju.mp - -// CentralNic : http://www.centralnic.com/names/domains -// Submitted by registry -ae.org -br.com -cn.com -com.de -com.se -de.com -eu.com -gb.net -hu.net -jp.net -jpn.com -mex.com -ru.com -sa.com -se.net -uk.com -uk.net -us.com -za.bz -za.com - -// No longer operated by CentralNic, these entries should be adopted and/or removed by current operators -// Submitted by Gavin Brown -ar.com -hu.com -kr.com -no.com -qc.com -uy.com - -// Africa.com Web Solutions Ltd : https://registry.africa.com -// Submitted by Gavin Brown -africa.com - -// iDOT Services Limited : http://www.domain.gr.com -// Submitted by Gavin Brown -gr.com - -// Radix FZC : http://domains.in.net -// Submitted by Gavin Brown -in.net -web.in - -// US REGISTRY LLC : http://us.org -// Submitted by Gavin Brown -us.org - -// co.com Registry, LLC : https://registry.co.com -// Submitted by Gavin Brown -co.com - -// Roar Domains LLC : https://roar.basketball/ -// Submitted by Gavin Brown -aus.basketball -nz.basketball - -// BRS Media : https://brsmedia.com/ -// Submitted by Gavin Brown -radio.am -radio.fm - -// c.la : http://www.c.la/ -c.la - -// certmgr.org : https://certmgr.org -// Submitted by B. Blechschmidt -certmgr.org - -// Cityhost LLC : https://cityhost.ua -// Submitted by Maksym Rivtin -cx.ua - -// Civilized Discourse Construction Kit, Inc. : https://www.discourse.org/ -// Submitted by Rishabh Nambiar & Michael Brown -discourse.group -discourse.team - -// Clever Cloud : https://www.clever-cloud.com/ -// Submitted by Quentin Adam -cleverapps.io - -// Clerk : https://www.clerk.dev -// Submitted by Colin Sidoti -clerk.app -clerkstage.app -*.lcl.dev -*.lclstage.dev -*.stg.dev -*.stgstage.dev - -// ClickRising : https://clickrising.com/ -// Submitted by Umut Gumeli -clickrising.net - -// Cloud66 : https://www.cloud66.com/ -// Submitted by Khash Sajadi -c66.me -cloud66.ws -cloud66.zone - -// CloudAccess.net : https://www.cloudaccess.net/ -// Submitted by Pawel Panek -jdevcloud.com -wpdevcloud.com -cloudaccess.host -freesite.host -cloudaccess.net - -// cloudControl : https://www.cloudcontrol.com/ -// Submitted by Tobias Wilken -cloudcontrolled.com -cloudcontrolapp.com - -// Cloudera, Inc. : https://www.cloudera.com/ -// Submitted by Kedarnath Waikar -*.cloudera.site - -// Cloudflare, Inc. : https://www.cloudflare.com/ -// Submitted by Cloudflare Team -cf-ipfs.com -cloudflare-ipfs.com -trycloudflare.com -pages.dev -r2.dev -workers.dev - -// Clovyr : https://clovyr.io -// Submitted by Patrick Nielsen -wnext.app - -// co.ca : http://registry.co.ca/ -co.ca - -// Co & Co : https://co-co.nl/ -// Submitted by Govert Versluis -*.otap.co - -// i-registry s.r.o. : http://www.i-registry.cz/ -// Submitted by Martin Semrad -co.cz - -// CDN77.com : http://www.cdn77.com -// Submitted by Jan Krpes -c.cdn77.org -cdn77-ssl.net -r.cdn77.net -rsc.cdn77.org -ssl.origin.cdn77-secure.org - -// Cloud DNS Ltd : http://www.cloudns.net -// Submitted by Aleksander Hristov -cloudns.asia -cloudns.biz -cloudns.club -cloudns.cc -cloudns.eu -cloudns.in -cloudns.info -cloudns.org -cloudns.pro -cloudns.pw -cloudns.us - -// CNPY : https://cnpy.gdn -// Submitted by Angelo Gladding -cnpy.gdn - -// Codeberg e. V. : https://codeberg.org -// Submitted by Moritz Marquardt -codeberg.page - -// CoDNS B.V. -co.nl -co.no - -// Combell.com : https://www.combell.com -// Submitted by Thomas Wouters -webhosting.be -hosting-cluster.nl - -// Coordination Center for TLD RU and XN--P1AI : https://cctld.ru/en/domains/domens_ru/reserved/ -// Submitted by George Georgievsky -ac.ru -edu.ru -gov.ru -int.ru -mil.ru -test.ru - -// COSIMO GmbH : http://www.cosimo.de -// Submitted by Rene Marticke -dyn.cosidns.de -dynamisches-dns.de -dnsupdater.de -internet-dns.de -l-o-g-i-n.de -dynamic-dns.info -feste-ip.net -knx-server.net -static-access.net - -// Craynic, s.r.o. : http://www.craynic.com/ -// Submitted by Ales Krajnik -realm.cz - -// Cryptonomic : https://cryptonomic.net/ -// Submitted by Andrew Cady -*.cryptonomic.net - -// Cupcake : https://cupcake.io/ -// Submitted by Jonathan Rudenberg -cupcake.is - -// Curv UG : https://curv-labs.de/ -// Submitted by Marvin Wiesner -curv.dev - -// Customer OCI - Oracle Dyn https://cloud.oracle.com/home https://dyn.com/dns/ -// Submitted by Gregory Drake -// Note: This is intended to also include customer-oci.com due to wildcards implicitly including the current label -*.customer-oci.com -*.oci.customer-oci.com -*.ocp.customer-oci.com -*.ocs.customer-oci.com - -// cyon GmbH : https://www.cyon.ch/ -// Submitted by Dominic Luechinger -cyon.link -cyon.site - -// Danger Science Group: https://dangerscience.com/ -// Submitted by Skylar MacDonald -fnwk.site -folionetwork.site -platform0.app - -// Daplie, Inc : https://daplie.com -// Submitted by AJ ONeal -daplie.me -localhost.daplie.me - -// Datto, Inc. : https://www.datto.com/ -// Submitted by Philipp Heckel -dattolocal.com -dattorelay.com -dattoweb.com -mydatto.com -dattolocal.net -mydatto.net - -// Dansk.net : http://www.dansk.net/ -// Submitted by Anani Voule -biz.dk -co.dk -firm.dk -reg.dk -store.dk - -// dappnode.io : https://dappnode.io/ -// Submitted by Abel Boldu / DAppNode Team -dyndns.dappnode.io - -// dapps.earth : https://dapps.earth/ -// Submitted by Daniil Burdakov -*.dapps.earth -*.bzz.dapps.earth - -// Dark, Inc. : https://darklang.com -// Submitted by Paul Biggar -builtwithdark.com - -// DataDetect, LLC. : https://datadetect.com -// Submitted by Andrew Banchich -demo.datadetect.com -instance.datadetect.com - -// Datawire, Inc : https://www.datawire.io -// Submitted by Richard Li -edgestack.me - -// DDNS5 : https://ddns5.com -// Submitted by Cameron Elliott -ddns5.com - -// Debian : https://www.debian.org/ -// Submitted by Peter Palfrader / Debian Sysadmin Team -debian.net - -// Deno Land Inc : https://deno.com/ -// Submitted by Luca Casonato -deno.dev -deno-staging.dev - -// deSEC : https://desec.io/ -// Submitted by Peter Thomassen -dedyn.io - -// Deta: https://www.deta.sh/ -// Submitted by Aavash Shrestha -deta.app -deta.dev - -// Diher Solutions : https://diher.solutions -// Submitted by Didi Hermawan -*.rss.my.id -*.diher.solutions - -// Discord Inc : https://discord.com -// Submitted by Sahn Lam -discordsays.com -discordsez.com - -// DNS Africa Ltd https://dns.business -// Submitted by Calvin Browne -jozi.biz - -// DNShome : https://www.dnshome.de/ -// Submitted by Norbert Auler -dnshome.de - -// DotArai : https://www.dotarai.com/ -// Submitted by Atsadawat Netcharadsang -online.th -shop.th - -// DrayTek Corp. : https://www.draytek.com/ -// Submitted by Paul Fang -drayddns.com - -// DreamCommerce : https://shoper.pl/ -// Submitted by Konrad Kotarba -shoparena.pl - -// DreamHost : http://www.dreamhost.com/ -// Submitted by Andrew Farmer -dreamhosters.com - -// Drobo : http://www.drobo.com/ -// Submitted by Ricardo Padilha -mydrobo.com - -// Drud Holdings, LLC. : https://www.drud.com/ -// Submitted by Kevin Bridges -drud.io -drud.us - -// DuckDNS : http://www.duckdns.org/ -// Submitted by Richard Harper -duckdns.org - -// Bip : https://bip.sh -// Submitted by Joel Kennedy -bip.sh - -// bitbridge.net : Submitted by Craig Welch, abeliidev@gmail.com -bitbridge.net - -// dy.fi : http://dy.fi/ -// Submitted by Heikki Hannikainen -dy.fi -tunk.org - -// DynDNS.com : http://www.dyndns.com/services/dns/dyndns/ -dyndns-at-home.com -dyndns-at-work.com -dyndns-blog.com -dyndns-free.com -dyndns-home.com -dyndns-ip.com -dyndns-mail.com -dyndns-office.com -dyndns-pics.com -dyndns-remote.com -dyndns-server.com -dyndns-web.com -dyndns-wiki.com -dyndns-work.com -dyndns.biz -dyndns.info -dyndns.org -dyndns.tv -at-band-camp.net -ath.cx -barrel-of-knowledge.info -barrell-of-knowledge.info -better-than.tv -blogdns.com -blogdns.net -blogdns.org -blogsite.org -boldlygoingnowhere.org -broke-it.net -buyshouses.net -cechire.com -dnsalias.com -dnsalias.net -dnsalias.org -dnsdojo.com -dnsdojo.net -dnsdojo.org -does-it.net -doesntexist.com -doesntexist.org -dontexist.com -dontexist.net -dontexist.org -doomdns.com -doomdns.org -dvrdns.org -dyn-o-saur.com -dynalias.com -dynalias.net -dynalias.org -dynathome.net -dyndns.ws -endofinternet.net -endofinternet.org -endoftheinternet.org -est-a-la-maison.com -est-a-la-masion.com -est-le-patron.com -est-mon-blogueur.com -for-better.biz -for-more.biz -for-our.info -for-some.biz -for-the.biz -forgot.her.name -forgot.his.name -from-ak.com -from-al.com -from-ar.com -from-az.net -from-ca.com -from-co.net -from-ct.com -from-dc.com -from-de.com -from-fl.com -from-ga.com -from-hi.com -from-ia.com -from-id.com -from-il.com -from-in.com -from-ks.com -from-ky.com -from-la.net -from-ma.com -from-md.com -from-me.org -from-mi.com -from-mn.com -from-mo.com -from-ms.com -from-mt.com -from-nc.com -from-nd.com -from-ne.com -from-nh.com -from-nj.com -from-nm.com -from-nv.com -from-ny.net -from-oh.com -from-ok.com -from-or.com -from-pa.com -from-pr.com -from-ri.com -from-sc.com -from-sd.com -from-tn.com -from-tx.com -from-ut.com -from-va.com -from-vt.com -from-wa.com -from-wi.com -from-wv.com -from-wy.com -ftpaccess.cc -fuettertdasnetz.de -game-host.org -game-server.cc -getmyip.com -gets-it.net -go.dyndns.org -gotdns.com -gotdns.org -groks-the.info -groks-this.info -ham-radio-op.net -here-for-more.info -hobby-site.com -hobby-site.org -home.dyndns.org -homedns.org -homeftp.net -homeftp.org -homeip.net -homelinux.com -homelinux.net -homelinux.org -homeunix.com -homeunix.net -homeunix.org -iamallama.com -in-the-band.net -is-a-anarchist.com -is-a-blogger.com -is-a-bookkeeper.com -is-a-bruinsfan.org -is-a-bulls-fan.com -is-a-candidate.org -is-a-caterer.com -is-a-celticsfan.org -is-a-chef.com -is-a-chef.net -is-a-chef.org -is-a-conservative.com -is-a-cpa.com -is-a-cubicle-slave.com -is-a-democrat.com -is-a-designer.com -is-a-doctor.com -is-a-financialadvisor.com -is-a-geek.com -is-a-geek.net -is-a-geek.org -is-a-green.com -is-a-guru.com -is-a-hard-worker.com -is-a-hunter.com -is-a-knight.org -is-a-landscaper.com -is-a-lawyer.com -is-a-liberal.com -is-a-libertarian.com -is-a-linux-user.org -is-a-llama.com -is-a-musician.com -is-a-nascarfan.com -is-a-nurse.com -is-a-painter.com -is-a-patsfan.org -is-a-personaltrainer.com -is-a-photographer.com -is-a-player.com -is-a-republican.com -is-a-rockstar.com -is-a-socialist.com -is-a-soxfan.org -is-a-student.com -is-a-teacher.com -is-a-techie.com -is-a-therapist.com -is-an-accountant.com -is-an-actor.com -is-an-actress.com -is-an-anarchist.com -is-an-artist.com -is-an-engineer.com -is-an-entertainer.com -is-by.us -is-certified.com -is-found.org -is-gone.com -is-into-anime.com -is-into-cars.com -is-into-cartoons.com -is-into-games.com -is-leet.com -is-lost.org -is-not-certified.com -is-saved.org -is-slick.com -is-uberleet.com -is-very-bad.org -is-very-evil.org -is-very-good.org -is-very-nice.org -is-very-sweet.org -is-with-theband.com -isa-geek.com -isa-geek.net -isa-geek.org -isa-hockeynut.com -issmarterthanyou.com -isteingeek.de -istmein.de -kicks-ass.net -kicks-ass.org -knowsitall.info -land-4-sale.us -lebtimnetz.de -leitungsen.de -likes-pie.com -likescandy.com -merseine.nu -mine.nu -misconfused.org -mypets.ws -myphotos.cc -neat-url.com -office-on-the.net -on-the-web.tv -podzone.net -podzone.org -readmyblog.org -saves-the-whales.com -scrapper-site.net -scrapping.cc -selfip.biz -selfip.com -selfip.info -selfip.net -selfip.org -sells-for-less.com -sells-for-u.com -sells-it.net -sellsyourhome.org -servebbs.com -servebbs.net -servebbs.org -serveftp.net -serveftp.org -servegame.org -shacknet.nu -simple-url.com -space-to-rent.com -stuff-4-sale.org -stuff-4-sale.us -teaches-yoga.com -thruhere.net -traeumtgerade.de -webhop.biz -webhop.info -webhop.net -webhop.org -worse-than.tv -writesthisblog.com - -// ddnss.de : https://www.ddnss.de/ -// Submitted by Robert Niedziela -ddnss.de -dyn.ddnss.de -dyndns.ddnss.de -dyndns1.de -dyn-ip24.de -home-webserver.de -dyn.home-webserver.de -myhome-server.de -ddnss.org - -// Definima : http://www.definima.com/ -// Submitted by Maxence Bitterli -definima.net -definima.io - -// DigitalOcean App Platform : https://www.digitalocean.com/products/app-platform/ -// Submitted by Braxton Huggins -ondigitalocean.app - -// DigitalOcean Spaces : https://www.digitalocean.com/products/spaces/ -// Submitted by Robin H. Johnson -*.digitaloceanspaces.com - -// dnstrace.pro : https://dnstrace.pro/ -// Submitted by Chris Partridge -bci.dnstrace.pro - -// Dynu.com : https://www.dynu.com/ -// Submitted by Sue Ye -ddnsfree.com -ddnsgeek.com -giize.com -gleeze.com -kozow.com -loseyourip.com -ooguy.com -theworkpc.com -casacam.net -dynu.net -accesscam.org -camdvr.org -freeddns.org -mywire.org -webredirect.org -myddns.rocks -blogsite.xyz - -// dynv6 : https://dynv6.com -// Submitted by Dominik Menke -dynv6.net - -// E4YOU spol. s.r.o. : https://e4you.cz/ -// Submitted by Vladimir Dudr -e4.cz - -// Easypanel : https://easypanel.io -// Submitted by Andrei Canta -easypanel.app -easypanel.host - -// Elementor : Elementor Ltd. -// Submitted by Anton Barkan -elementor.cloud -elementor.cool - -// En root‽ : https://en-root.org -// Submitted by Emmanuel Raviart -en-root.fr - -// Enalean SAS: https://www.enalean.com -// Submitted by Thomas Cottier -mytuleap.com -tuleap-partners.com - -// Encoretivity AB: https://encore.dev -// Submitted by André Eriksson -encr.app -encoreapi.com - -// ECG Robotics, Inc: https://ecgrobotics.org -// Submitted by -onred.one -staging.onred.one - -// encoway GmbH : https://www.encoway.de -// Submitted by Marcel Daus -eu.encoway.cloud - -// EU.org https://eu.org/ -// Submitted by Pierre Beyssac -eu.org -al.eu.org -asso.eu.org -at.eu.org -au.eu.org -be.eu.org -bg.eu.org -ca.eu.org -cd.eu.org -ch.eu.org -cn.eu.org -cy.eu.org -cz.eu.org -de.eu.org -dk.eu.org -edu.eu.org -ee.eu.org -es.eu.org -fi.eu.org -fr.eu.org -gr.eu.org -hr.eu.org -hu.eu.org -ie.eu.org -il.eu.org -in.eu.org -int.eu.org -is.eu.org -it.eu.org -jp.eu.org -kr.eu.org -lt.eu.org -lu.eu.org -lv.eu.org -mc.eu.org -me.eu.org -mk.eu.org -mt.eu.org -my.eu.org -net.eu.org -ng.eu.org -nl.eu.org -no.eu.org -nz.eu.org -paris.eu.org -pl.eu.org -pt.eu.org -q-a.eu.org -ro.eu.org -ru.eu.org -se.eu.org -si.eu.org -sk.eu.org -tr.eu.org -uk.eu.org -us.eu.org - -// Eurobyte : https://eurobyte.ru -// Submitted by Evgeniy Subbotin -eurodir.ru - -// Evennode : http://www.evennode.com/ -// Submitted by Michal Kralik -eu-1.evennode.com -eu-2.evennode.com -eu-3.evennode.com -eu-4.evennode.com -us-1.evennode.com -us-2.evennode.com -us-3.evennode.com -us-4.evennode.com - -// eDirect Corp. : https://hosting.url.com.tw/ -// Submitted by C.S. chang -twmail.cc -twmail.net -twmail.org -mymailer.com.tw -url.tw - -// Fabrica Technologies, Inc. : https://www.fabrica.dev/ -// Submitted by Eric Jiang -onfabrica.com - -// Facebook, Inc. -// Submitted by Peter Ruibal -apps.fbsbx.com - -// FAITID : https://faitid.org/ -// Submitted by Maxim Alzoba -// https://www.flexireg.net/stat_info -ru.net -adygeya.ru -bashkiria.ru -bir.ru -cbg.ru -com.ru -dagestan.ru -grozny.ru -kalmykia.ru -kustanai.ru -marine.ru -mordovia.ru -msk.ru -mytis.ru -nalchik.ru -nov.ru -pyatigorsk.ru -spb.ru -vladikavkaz.ru -vladimir.ru -abkhazia.su -adygeya.su -aktyubinsk.su -arkhangelsk.su -armenia.su -ashgabad.su -azerbaijan.su -balashov.su -bashkiria.su -bryansk.su -bukhara.su -chimkent.su -dagestan.su -east-kazakhstan.su -exnet.su -georgia.su -grozny.su -ivanovo.su -jambyl.su -kalmykia.su -kaluga.su -karacol.su -karaganda.su -karelia.su -khakassia.su -krasnodar.su -kurgan.su -kustanai.su -lenug.su -mangyshlak.su -mordovia.su -msk.su -murmansk.su -nalchik.su -navoi.su -north-kazakhstan.su -nov.su -obninsk.su -penza.su -pokrovsk.su -sochi.su -spb.su -tashkent.su -termez.su -togliatti.su -troitsk.su -tselinograd.su -tula.su -tuva.su -vladikavkaz.su -vladimir.su -vologda.su - -// Fancy Bits, LLC : http://getchannels.com -// Submitted by Aman Gupta -channelsdvr.net -u.channelsdvr.net - -// Fastly Inc. : http://www.fastly.com/ -// Submitted by Fastly Security -edgecompute.app -fastly-edge.com -fastly-terrarium.com -fastlylb.net -map.fastlylb.net -freetls.fastly.net -map.fastly.net -a.prod.fastly.net -global.prod.fastly.net -a.ssl.fastly.net -b.ssl.fastly.net -global.ssl.fastly.net - -// Fastmail : https://www.fastmail.com/ -// Submitted by Marc Bradshaw -*.user.fm - -// FASTVPS EESTI OU : https://fastvps.ru/ -// Submitted by Likhachev Vasiliy -fastvps-server.com -fastvps.host -myfast.host -fastvps.site -myfast.space - -// Fedora : https://fedoraproject.org/ -// submitted by Patrick Uiterwijk -fedorainfracloud.org -fedorapeople.org -cloud.fedoraproject.org -app.os.fedoraproject.org -app.os.stg.fedoraproject.org - -// FearWorks Media Ltd. : https://fearworksmedia.co.uk -// submitted by Keith Fairley -conn.uk -copro.uk -hosp.uk - -// Fermax : https://fermax.com/ -// submitted by Koen Van Isterdael -mydobiss.com - -// FH Muenster : https://www.fh-muenster.de -// Submitted by Robin Naundorf -fh-muenster.io - -// Filegear Inc. : https://www.filegear.com -// Submitted by Jason Zhu -filegear.me -filegear-au.me -filegear-de.me -filegear-gb.me -filegear-ie.me -filegear-jp.me -filegear-sg.me - -// Firebase, Inc. -// Submitted by Chris Raynor -firebaseapp.com - -// Firewebkit : https://www.firewebkit.com -// Submitted by Majid Qureshi -fireweb.app - -// FLAP : https://www.flap.cloud -// Submitted by Louis Chemineau -flap.id - -// FlashDrive : https://flashdrive.io -// Submitted by Eric Chan -onflashdrive.app -fldrv.com - -// fly.io: https://fly.io -// Submitted by Kurt Mackey -fly.dev -edgeapp.net -shw.io - -// Flynn : https://flynn.io -// Submitted by Jonathan Rudenberg -flynnhosting.net - -// Forgerock : https://www.forgerock.com -// Submitted by Roderick Parr -forgeblocks.com -id.forgerock.io - -// Framer : https://www.framer.com -// Submitted by Koen Rouwhorst -framer.app -framercanvas.com -framer.media -framer.photos -framer.website -framer.wiki - -// Frusky MEDIA&PR : https://www.frusky.de -// Submitted by Victor Pupynin -*.frusky.de - -// RavPage : https://www.ravpage.co.il -// Submitted by Roni Horowitz -ravpage.co.il - -// Frederik Braun https://frederik-braun.com -// Submitted by Frederik Braun -0e.vc - -// Freebox : http://www.freebox.fr -// Submitted by Romain Fliedel -freebox-os.com -freeboxos.com -fbx-os.fr -fbxos.fr -freebox-os.fr -freeboxos.fr - -// freedesktop.org : https://www.freedesktop.org -// Submitted by Daniel Stone -freedesktop.org - -// freemyip.com : https://freemyip.com -// Submitted by Cadence -freemyip.com - -// FunkFeuer - Verein zur Förderung freier Netze : https://www.funkfeuer.at -// Submitted by Daniel A. Maierhofer -wien.funkfeuer.at - -// Futureweb OG : http://www.futureweb.at -// Submitted by Andreas Schnederle-Wagner -*.futurecms.at -*.ex.futurecms.at -*.in.futurecms.at -futurehosting.at -futuremailing.at -*.ex.ortsinfo.at -*.kunden.ortsinfo.at -*.statics.cloud - -// GDS : https://www.gov.uk/service-manual/technology/managing-domain-names -// Submitted by Stephen Ford -independent-commission.uk -independent-inquest.uk -independent-inquiry.uk -independent-panel.uk -independent-review.uk -public-inquiry.uk -royal-commission.uk -campaign.gov.uk -service.gov.uk - -// CDDO : https://www.gov.uk/guidance/get-an-api-domain-on-govuk -// Submitted by Jamie Tanna -api.gov.uk - -// Gehirn Inc. : https://www.gehirn.co.jp/ -// Submitted by Kohei YOSHIDA -gehirn.ne.jp -usercontent.jp - -// Gentlent, Inc. : https://www.gentlent.com -// Submitted by Tom Klein -gentapps.com -gentlentapis.com -lab.ms -cdn-edges.net - -// Ghost Foundation : https://ghost.org -// Submitted by Matt Hanley -ghost.io - -// GignoSystemJapan: http://gsj.bz -// Submitted by GignoSystemJapan -gsj.bz - -// GitHub, Inc. -// Submitted by Patrick Toomey -githubusercontent.com -githubpreview.dev -github.io - -// GitLab, Inc. -// Submitted by Alex Hanselka -gitlab.io - -// Gitplac.si - https://gitplac.si -// Submitted by Aljaž Starc -gitapp.si -gitpage.si - -// Glitch, Inc : https://glitch.com -// Submitted by Mads Hartmann -glitch.me - -// Global NOG Alliance : https://nogalliance.org/ -// Submitted by Sander Steffann -nog.community - -// Globe Hosting SRL : https://www.globehosting.com/ -// Submitted by Gavin Brown -co.ro -shop.ro - -// GMO Pepabo, Inc. : https://pepabo.com/ -// Submitted by Hosting Div -lolipop.io -angry.jp -babyblue.jp -babymilk.jp -backdrop.jp -bambina.jp -bitter.jp -blush.jp -boo.jp -boy.jp -boyfriend.jp -but.jp -candypop.jp -capoo.jp -catfood.jp -cheap.jp -chicappa.jp -chillout.jp -chips.jp -chowder.jp -chu.jp -ciao.jp -cocotte.jp -coolblog.jp -cranky.jp -cutegirl.jp -daa.jp -deca.jp -deci.jp -digick.jp -egoism.jp -fakefur.jp -fem.jp -flier.jp -floppy.jp -fool.jp -frenchkiss.jp -girlfriend.jp -girly.jp -gloomy.jp -gonna.jp -greater.jp -hacca.jp -heavy.jp -her.jp -hiho.jp -hippy.jp -holy.jp -hungry.jp -icurus.jp -itigo.jp -jellybean.jp -kikirara.jp -kill.jp -kilo.jp -kuron.jp -littlestar.jp -lolipopmc.jp -lolitapunk.jp -lomo.jp -lovepop.jp -lovesick.jp -main.jp -mods.jp -mond.jp -mongolian.jp -moo.jp -namaste.jp -nikita.jp -nobushi.jp -noor.jp -oops.jp -parallel.jp -parasite.jp -pecori.jp -peewee.jp -penne.jp -pepper.jp -perma.jp -pigboat.jp -pinoko.jp -punyu.jp -pupu.jp -pussycat.jp -pya.jp -raindrop.jp -readymade.jp -sadist.jp -schoolbus.jp -secret.jp -staba.jp -stripper.jp -sub.jp -sunnyday.jp -thick.jp -tonkotsu.jp -under.jp -upper.jp -velvet.jp -verse.jp -versus.jp -vivian.jp -watson.jp -weblike.jp -whitesnow.jp -zombie.jp -heteml.net - -// GOV.UK Platform as a Service : https://www.cloud.service.gov.uk/ -// Submitted by Tom Whitwell -cloudapps.digital -london.cloudapps.digital - -// GOV.UK Pay : https://www.payments.service.gov.uk/ -// Submitted by Richard Baker -pymnt.uk - -// UKHomeOffice : https://www.gov.uk/government/organisations/home-office -// Submitted by Jon Shanks -homeoffice.gov.uk - -// GlobeHosting, Inc. -// Submitted by Zoltan Egresi -ro.im - -// GoIP DNS Services : http://www.goip.de -// Submitted by Christian Poulter -goip.de - -// Google, Inc. -// Submitted by Eduardo Vela -run.app -a.run.app -web.app -*.0emm.com -appspot.com -*.r.appspot.com -codespot.com -googleapis.com -googlecode.com -pagespeedmobilizer.com -publishproxy.com -withgoogle.com -withyoutube.com -*.gateway.dev -cloud.goog -translate.goog -*.usercontent.goog -cloudfunctions.net -blogspot.ae -blogspot.al -blogspot.am -blogspot.ba -blogspot.be -blogspot.bg -blogspot.bj -blogspot.ca -blogspot.cf -blogspot.ch -blogspot.cl -blogspot.co.at -blogspot.co.id -blogspot.co.il -blogspot.co.ke -blogspot.co.nz -blogspot.co.uk -blogspot.co.za -blogspot.com -blogspot.com.ar -blogspot.com.au -blogspot.com.br -blogspot.com.by -blogspot.com.co -blogspot.com.cy -blogspot.com.ee -blogspot.com.eg -blogspot.com.es -blogspot.com.mt -blogspot.com.ng -blogspot.com.tr -blogspot.com.uy -blogspot.cv -blogspot.cz -blogspot.de -blogspot.dk -blogspot.fi -blogspot.fr -blogspot.gr -blogspot.hk -blogspot.hr -blogspot.hu -blogspot.ie -blogspot.in -blogspot.is -blogspot.it -blogspot.jp -blogspot.kr -blogspot.li -blogspot.lt -blogspot.lu -blogspot.md -blogspot.mk -blogspot.mr -blogspot.mx -blogspot.my -blogspot.nl -blogspot.no -blogspot.pe -blogspot.pt -blogspot.qa -blogspot.re -blogspot.ro -blogspot.rs -blogspot.ru -blogspot.se -blogspot.sg -blogspot.si -blogspot.sk -blogspot.sn -blogspot.td -blogspot.tw -blogspot.ug -blogspot.vn - -// Goupile : https://goupile.fr -// Submitted by Niels Martignene -goupile.fr - -// Government of the Netherlands: https://www.government.nl -// Submitted by -gov.nl - -// Group 53, LLC : https://www.group53.com -// Submitted by Tyler Todd -awsmppl.com - -// GünstigBestellen : https://günstigbestellen.de -// Submitted by Furkan Akkoc -günstigbestellen.de -günstigliefern.de - -// Hakaran group: http://hakaran.cz -// Submitted by Arseniy Sokolov -fin.ci -free.hr -caa.li -ua.rs -conf.se - -// Handshake : https://handshake.org -// Submitted by Mike Damm -hs.zone -hs.run - -// Hashbang : https://hashbang.sh -hashbang.sh - -// Hasura : https://hasura.io -// Submitted by Shahidh K Muhammed -hasura.app -hasura-app.io - -// Heilbronn University of Applied Sciences - Faculty Informatics (GitLab Pages): https://www.hs-heilbronn.de -// Submitted by Richard Zowalla -pages.it.hs-heilbronn.de - -// Hepforge : https://www.hepforge.org -// Submitted by David Grellscheid -hepforge.org - -// Heroku : https://www.heroku.com/ -// Submitted by Tom Maher -herokuapp.com -herokussl.com - -// Hibernating Rhinos -// Submitted by Oren Eini -ravendb.cloud -ravendb.community -ravendb.me -development.run -ravendb.run - -// home.pl S.A.: https://home.pl -// Submitted by Krzysztof Wolski -homesklep.pl - -// Hong Kong Productivity Council: https://www.hkpc.org/ -// Submitted by SECaaS Team -secaas.hk - -// Hoplix : https://www.hoplix.com -// Submitted by Danilo De Franco -hoplix.shop - - -// HOSTBIP REGISTRY : https://www.hostbip.com/ -// Submitted by Atanunu Igbunuroghene -orx.biz -biz.gl -col.ng -firm.ng -gen.ng -ltd.ng -ngo.ng -edu.scot -sch.so - -// HostFly : https://www.ie.ua -// Submitted by Bohdan Dub -ie.ua - -// HostyHosting (hostyhosting.com) -hostyhosting.io - -// Häkkinen.fi -// Submitted by Eero Häkkinen -häkkinen.fi - -// Ici la Lune : http://www.icilalune.com/ -// Submitted by Simon Morvan -*.moonscale.io -moonscale.net - -// iki.fi -// Submitted by Hannu Aronsson -iki.fi - -// iliad italia: https://www.iliad.it -// Submitted by Marios Makassikis -ibxos.it -iliadboxos.it - -// Impertrix Solutions : -// Submitted by Zhixiang Zhao -impertrixcdn.com -impertrix.com - -// Incsub, LLC: https://incsub.com/ -// Submitted by Aaron Edwards -smushcdn.com -wphostedmail.com -wpmucdn.com -tempurl.host -wpmudev.host - -// Individual Network Berlin e.V. : https://www.in-berlin.de/ -// Submitted by Christian Seitz -dyn-berlin.de -in-berlin.de -in-brb.de -in-butter.de -in-dsl.de -in-dsl.net -in-dsl.org -in-vpn.de -in-vpn.net -in-vpn.org - -// info.at : http://www.info.at/ -biz.at -info.at - -// info.cx : http://info.cx -// Submitted by Jacob Slater -info.cx - -// Interlegis : http://www.interlegis.leg.br -// Submitted by Gabriel Ferreira -ac.leg.br -al.leg.br -am.leg.br -ap.leg.br -ba.leg.br -ce.leg.br -df.leg.br -es.leg.br -go.leg.br -ma.leg.br -mg.leg.br -ms.leg.br -mt.leg.br -pa.leg.br -pb.leg.br -pe.leg.br -pi.leg.br -pr.leg.br -rj.leg.br -rn.leg.br -ro.leg.br -rr.leg.br -rs.leg.br -sc.leg.br -se.leg.br -sp.leg.br -to.leg.br - -// intermetrics GmbH : https://pixolino.com/ -// Submitted by Wolfgang Schwarz -pixolino.com - -// Internet-Pro, LLP: https://netangels.ru/ -// Submitted by Vasiliy Sheredeko -na4u.ru - -// iopsys software solutions AB : https://iopsys.eu/ -// Submitted by Roman Azarenko -iopsys.se - -// IPiFony Systems, Inc. : https://www.ipifony.com/ -// Submitted by Matthew Hardeman -ipifony.net - -// IServ GmbH : https://iserv.de -// Submitted by Mario Hoberg -iservschule.de -mein-iserv.de -schulplattform.de -schulserver.de -test-iserv.de -iserv.dev - -// I-O DATA DEVICE, INC. : http://www.iodata.com/ -// Submitted by Yuji Minagawa -iobb.net - -// Jelastic, Inc. : https://jelastic.com/ -// Submitted by Ihor Kolodyuk -mel.cloudlets.com.au -cloud.interhostsolutions.be -users.scale.virtualcloud.com.br -mycloud.by -alp1.ae.flow.ch -appengine.flow.ch -es-1.axarnet.cloud -diadem.cloud -vip.jelastic.cloud -jele.cloud -it1.eur.aruba.jenv-aruba.cloud -it1.jenv-aruba.cloud -keliweb.cloud -cs.keliweb.cloud -oxa.cloud -tn.oxa.cloud -uk.oxa.cloud -primetel.cloud -uk.primetel.cloud -ca.reclaim.cloud -uk.reclaim.cloud -us.reclaim.cloud -ch.trendhosting.cloud -de.trendhosting.cloud -jele.club -amscompute.com -clicketcloud.com -dopaas.com -hidora.com -paas.hosted-by-previder.com -rag-cloud.hosteur.com -rag-cloud-ch.hosteur.com -jcloud.ik-server.com -jcloud-ver-jpc.ik-server.com -demo.jelastic.com -kilatiron.com -paas.massivegrid.com -jed.wafaicloud.com -lon.wafaicloud.com -ryd.wafaicloud.com -j.scaleforce.com.cy -jelastic.dogado.eu -fi.cloudplatform.fi -demo.datacenter.fi -paas.datacenter.fi -jele.host -mircloud.host -paas.beebyte.io -sekd1.beebyteapp.io -jele.io -cloud-fr1.unispace.io -jc.neen.it -cloud.jelastic.open.tim.it -jcloud.kz -upaas.kazteleport.kz -cloudjiffy.net -fra1-de.cloudjiffy.net -west1-us.cloudjiffy.net -jls-sto1.elastx.net -jls-sto2.elastx.net -jls-sto3.elastx.net -faststacks.net -fr-1.paas.massivegrid.net -lon-1.paas.massivegrid.net -lon-2.paas.massivegrid.net -ny-1.paas.massivegrid.net -ny-2.paas.massivegrid.net -sg-1.paas.massivegrid.net -jelastic.saveincloud.net -nordeste-idc.saveincloud.net -j.scaleforce.net -jelastic.tsukaeru.net -sdscloud.pl -unicloud.pl -mircloud.ru -jelastic.regruhosting.ru -enscaled.sg -jele.site -jelastic.team -orangecloud.tn -j.layershift.co.uk -phx.enscaled.us -mircloud.us - -// Jino : https://www.jino.ru -// Submitted by Sergey Ulyashin -myjino.ru -*.hosting.myjino.ru -*.landing.myjino.ru -*.spectrum.myjino.ru -*.vps.myjino.ru - -// Jotelulu S.L. : https://jotelulu.com -// Submitted by Daniel Fariña -jotelulu.cloud - -// Joyent : https://www.joyent.com/ -// Submitted by Brian Bennett -*.triton.zone -*.cns.joyent.com - -// JS.ORG : http://dns.js.org -// Submitted by Stefan Keim -js.org - -// KaasHosting : http://www.kaashosting.nl/ -// Submitted by Wouter Bakker -kaas.gg -khplay.nl - -// Kakao : https://www.kakaocorp.com/ -// Submitted by JaeYoong Lee -ktistory.com - -// Kapsi : https://kapsi.fi -// Submitted by Tomi Juntunen -kapsi.fi - -// Keyweb AG : https://www.keyweb.de -// Submitted by Martin Dannehl -keymachine.de - -// KingHost : https://king.host -// Submitted by Felipe Keller Braz -kinghost.net -uni5.net - -// KnightPoint Systems, LLC : http://www.knightpoint.com/ -// Submitted by Roy Keene -knightpoint.systems - -// KoobinEvent, SL: https://www.koobin.com -// Submitted by Iván Oliva -koobin.events - -// KUROKU LTD : https://kuroku.ltd/ -// Submitted by DisposaBoy -oya.to - -// Katholieke Universiteit Leuven: https://www.kuleuven.be -// Submitted by Abuse KU Leuven -kuleuven.cloud -ezproxy.kuleuven.be - -// .KRD : http://nic.krd/data/krd/Registration%20Policy.pdf -co.krd -edu.krd - -// Krellian Ltd. : https://krellian.com -// Submitted by Ben Francis -krellian.net -webthings.io - -// LCube - Professional hosting e.K. : https://www.lcube-webhosting.de -// Submitted by Lars Laehn -git-repos.de -lcube-server.de -svn-repos.de - -// Leadpages : https://www.leadpages.net -// Submitted by Greg Dallavalle -leadpages.co -lpages.co -lpusercontent.com - -// Lelux.fi : https://lelux.fi/ -// Submitted by Lelux Admin -lelux.site - -// Lifetime Hosting : https://Lifetime.Hosting/ -// Submitted by Mike Fillator -co.business -co.education -co.events -co.financial -co.network -co.place -co.technology - -// Lightmaker Property Manager, Inc. : https://app.lmpm.com/ -// Submitted by Greg Holland -app.lmpm.com - -// linkyard ldt: https://www.linkyard.ch/ -// Submitted by Mario Siegenthaler -linkyard.cloud -linkyard-cloud.ch - -// Linode : https://linode.com -// Submitted by -members.linode.com -*.nodebalancer.linode.com -*.linodeobjects.com -ip.linodeusercontent.com - -// LiquidNet Ltd : http://www.liquidnetlimited.com/ -// Submitted by Victor Velchev -we.bs - -// Localcert : https://localcert.dev -// Submitted by Lann Martin -*.user.localcert.dev - -// localzone.xyz -// Submitted by Kenny Niehage -localzone.xyz - -// Log'in Line : https://www.loginline.com/ -// Submitted by Rémi Mach -loginline.app -loginline.dev -loginline.io -loginline.services -loginline.site - -// Lokalized : https://lokalized.nl -// Submitted by Noah Taheij -servers.run - -// Lõhmus Family, The -// Submitted by Heiki Lõhmus -lohmus.me - -// LubMAN UMCS Sp. z o.o : https://lubman.pl/ -// Submitted by Ireneusz Maliszewski -krasnik.pl -leczna.pl -lubartow.pl -lublin.pl -poniatowa.pl -swidnik.pl - -// Lug.org.uk : https://lug.org.uk -// Submitted by Jon Spriggs -glug.org.uk -lug.org.uk -lugs.org.uk - -// Lukanet Ltd : https://lukanet.com -// Submitted by Anton Avramov -barsy.bg -barsy.co.uk -barsyonline.co.uk -barsycenter.com -barsyonline.com -barsy.club -barsy.de -barsy.eu -barsy.in -barsy.info -barsy.io -barsy.me -barsy.menu -barsy.mobi -barsy.net -barsy.online -barsy.org -barsy.pro -barsy.pub -barsy.ro -barsy.shop -barsy.site -barsy.support -barsy.uk - -// Magento Commerce -// Submitted by Damien Tournoud -*.magentosite.cloud - -// May First - People Link : https://mayfirst.org/ -// Submitted by Jamie McClelland -mayfirst.info -mayfirst.org - -// Mail.Ru Group : https://hb.cldmail.ru -// Submitted by Ilya Zaretskiy -hb.cldmail.ru - -// Mail Transfer Platform : https://www.neupeer.com -// Submitted by Li Hui -cn.vu - -// Maze Play: https://www.mazeplay.com -// Submitted by Adam Humpherys -mazeplay.com - -// mcpe.me : https://mcpe.me -// Submitted by Noa Heyl -mcpe.me - -// McHost : https://mchost.ru -// Submitted by Evgeniy Subbotin -mcdir.me -mcdir.ru -mcpre.ru -vps.mcdir.ru - -// Mediatech : https://mediatech.by -// Submitted by Evgeniy Kozhuhovskiy -mediatech.by -mediatech.dev - -// Medicom Health : https://medicomhealth.com -// Submitted by Michael Olson -hra.health - -// Memset hosting : https://www.memset.com -// Submitted by Tom Whitwell -miniserver.com -memset.net - -// Messerli Informatik AG : https://www.messerli.ch/ -// Submitted by Ruben Schmidmeister -messerli.app - -// MetaCentrum, CESNET z.s.p.o. : https://www.metacentrum.cz/en/ -// Submitted by Zdeněk Šustr -*.cloud.metacentrum.cz -custom.metacentrum.cz - -// MetaCentrum, CESNET z.s.p.o. : https://www.metacentrum.cz/en/ -// Submitted by Radim Janča -flt.cloud.muni.cz -usr.cloud.muni.cz - -// Meteor Development Group : https://www.meteor.com/hosting -// Submitted by Pierre Carrier -meteorapp.com -eu.meteorapp.com - -// Michau Enterprises Limited : http://www.co.pl/ -co.pl - -// Microsoft Corporation : http://microsoft.com -// Submitted by Public Suffix List Admin -*.azurecontainer.io -azurewebsites.net -azure-mobile.net -cloudapp.net -azurestaticapps.net -1.azurestaticapps.net -2.azurestaticapps.net -3.azurestaticapps.net -centralus.azurestaticapps.net -eastasia.azurestaticapps.net -eastus2.azurestaticapps.net -westeurope.azurestaticapps.net -westus2.azurestaticapps.net - -// minion.systems : http://minion.systems -// Submitted by Robert Böttinger -csx.cc - -// Mintere : https://mintere.com/ -// Submitted by Ben Aubin -mintere.site - -// MobileEducation, LLC : https://joinforte.com -// Submitted by Grayson Martin -forte.id - -// Mozilla Corporation : https://mozilla.com -// Submitted by Ben Francis -mozilla-iot.org - -// Mozilla Foundation : https://mozilla.org/ -// Submitted by glob -bmoattachments.org - -// MSK-IX : https://www.msk-ix.ru/ -// Submitted by Khannanov Roman -net.ru -org.ru -pp.ru - -// Mythic Beasts : https://www.mythic-beasts.com -// Submitted by Paul Cammish -hostedpi.com -customer.mythic-beasts.com -caracal.mythic-beasts.com -fentiger.mythic-beasts.com -lynx.mythic-beasts.com -ocelot.mythic-beasts.com -oncilla.mythic-beasts.com -onza.mythic-beasts.com -sphinx.mythic-beasts.com -vs.mythic-beasts.com -x.mythic-beasts.com -yali.mythic-beasts.com -cust.retrosnub.co.uk - -// Nabu Casa : https://www.nabucasa.com -// Submitted by Paulus Schoutsen -ui.nabu.casa - -// Net at Work Gmbh : https://www.netatwork.de -// Submitted by Jan Jaeschke -cloud.nospamproxy.com - -// Netlify : https://www.netlify.com -// Submitted by Jessica Parsons -netlify.app - -// Neustar Inc. -// Submitted by Trung Tran -4u.com - -// ngrok : https://ngrok.com/ -// Submitted by Alan Shreve -ngrok.app -ngrok-free.app -ngrok.dev -ngrok-free.dev -ngrok.io -ap.ngrok.io -au.ngrok.io -eu.ngrok.io -in.ngrok.io -jp.ngrok.io -sa.ngrok.io -us.ngrok.io -ngrok.pizza - -// Nimbus Hosting Ltd. : https://www.nimbushosting.co.uk/ -// Submitted by Nicholas Ford -nh-serv.co.uk - -// NFSN, Inc. : https://www.NearlyFreeSpeech.NET/ -// Submitted by Jeff Wheelhouse -nfshost.com - -// Noop : https://noop.app -// Submitted by Nathaniel Schweinberg -*.developer.app -noop.app - -// Northflank Ltd. : https://northflank.com/ -// Submitted by Marco Suter -*.northflank.app -*.build.run -*.code.run -*.database.run -*.migration.run - -// Noticeable : https://noticeable.io -// Submitted by Laurent Pellegrino -noticeable.news - -// Now-DNS : https://now-dns.com -// Submitted by Steve Russell -dnsking.ch -mypi.co -n4t.co -001www.com -ddnslive.com -myiphost.com -forumz.info -16-b.it -32-b.it -64-b.it -soundcast.me -tcp4.me -dnsup.net -hicam.net -now-dns.net -ownip.net -vpndns.net -dynserv.org -now-dns.org -x443.pw -now-dns.top -ntdll.top -freeddns.us -crafting.xyz -zapto.xyz - -// nsupdate.info : https://www.nsupdate.info/ -// Submitted by Thomas Waldmann -nsupdate.info -nerdpol.ovh - -// No-IP.com : https://noip.com/ -// Submitted by Deven Reza -blogsyte.com -brasilia.me -cable-modem.org -ciscofreak.com -collegefan.org -couchpotatofries.org -damnserver.com -ddns.me -ditchyourip.com -dnsfor.me -dnsiskinky.com -dvrcam.info -dynns.com -eating-organic.net -fantasyleague.cc -geekgalaxy.com -golffan.us -health-carereform.com -homesecuritymac.com -homesecuritypc.com -hopto.me -ilovecollege.info -loginto.me -mlbfan.org -mmafan.biz -myactivedirectory.com -mydissent.net -myeffect.net -mymediapc.net -mypsx.net -mysecuritycamera.com -mysecuritycamera.net -mysecuritycamera.org -net-freaks.com -nflfan.org -nhlfan.net -no-ip.ca -no-ip.co.uk -no-ip.net -noip.us -onthewifi.com -pgafan.net -point2this.com -pointto.us -privatizehealthinsurance.net -quicksytes.com -read-books.org -securitytactics.com -serveexchange.com -servehumour.com -servep2p.com -servesarcasm.com -stufftoread.com -ufcfan.org -unusualperson.com -workisboring.com -3utilities.com -bounceme.net -ddns.net -ddnsking.com -gotdns.ch -hopto.org -myftp.biz -myftp.org -myvnc.com -no-ip.biz -no-ip.info -no-ip.org -noip.me -redirectme.net -servebeer.com -serveblog.net -servecounterstrike.com -serveftp.com -servegame.com -servehalflife.com -servehttp.com -serveirc.com -serveminecraft.net -servemp3.com -servepics.com -servequake.com -sytes.net -webhop.me -zapto.org - -// NodeArt : https://nodeart.io -// Submitted by Konstantin Nosov -stage.nodeart.io - -// Nucleos Inc. : https://nucleos.com -// Submitted by Piotr Zduniak -pcloud.host - -// NYC.mn : http://www.information.nyc.mn -// Submitted by Matthew Brown -nyc.mn - -// Observable, Inc. : https://observablehq.com -// Submitted by Mike Bostock -static.observableusercontent.com - -// Octopodal Solutions, LLC. : https://ulterius.io/ -// Submitted by Andrew Sampson -cya.gg - -// OMG.LOL : -// Submitted by Adam Newbold -omg.lol - -// Omnibond Systems, LLC. : https://www.omnibond.com -// Submitted by Cole Estep -cloudycluster.net - -// OmniWe Limited: https://omniwe.com -// Submitted by Vicary Archangel -omniwe.site - -// One.com: https://www.one.com/ -// Submitted by Jacob Bunk Nielsen -123hjemmeside.dk -123hjemmeside.no -123homepage.it -123kotisivu.fi -123minsida.se -123miweb.es -123paginaweb.pt -123sait.ru -123siteweb.fr -123webseite.at -123webseite.de -123website.be -123website.ch -123website.lu -123website.nl -service.one -simplesite.com -simplesite.com.br -simplesite.gr -simplesite.pl - -// One Fold Media : http://www.onefoldmedia.com/ -// Submitted by Eddie Jones -nid.io - -// Open Social : https://www.getopensocial.com/ -// Submitted by Alexander Varwijk -opensocial.site - -// OpenCraft GmbH : http://opencraft.com/ -// Submitted by Sven Marnach -opencraft.hosting - -// OpenResearch GmbH: https://openresearch.com/ -// Submitted by Philipp Schmid -orsites.com - -// Opera Software, A.S.A. -// Submitted by Yngve Pettersen -operaunite.com - -// Orange : https://www.orange.com -// Submitted by Alexandre Linte -tech.orange - -// Oursky Limited : https://authgear.com/, https://skygear.io/ -// Submitted by Authgear Team , Skygear Developer -authgear-staging.com -authgearapps.com -skygearapp.com - -// OutSystems -// Submitted by Duarte Santos -outsystemscloud.com - -// OVHcloud: https://ovhcloud.com -// Submitted by Vincent Cassé -*.webpaas.ovh.net -*.hosting.ovh.net - -// OwnProvider GmbH: http://www.ownprovider.com -// Submitted by Jan Moennich -ownprovider.com -own.pm - -// OwO : https://whats-th.is/ -// Submitted by Dean Sheather -*.owo.codes - -// OX : http://www.ox.rs -// Submitted by Adam Grand -ox.rs - -// oy.lc -// Submitted by Charly Coste -oy.lc - -// Pagefog : https://pagefog.com/ -// Submitted by Derek Myers -pgfog.com - -// Pagefront : https://www.pagefronthq.com/ -// Submitted by Jason Kriss -pagefrontapp.com - -// PageXL : https://pagexl.com -// Submitted by Yann Guichard -pagexl.com - -// Paywhirl, Inc : https://paywhirl.com/ -// Submitted by Daniel Netzer -*.paywhirl.com - -// pcarrier.ca Software Inc: https://pcarrier.ca/ -// Submitted by Pierre Carrier -bar0.net -bar1.net -bar2.net -rdv.to - -// .pl domains (grandfathered) -art.pl -gliwice.pl -krakow.pl -poznan.pl -wroc.pl -zakopane.pl - -// Pantheon Systems, Inc. : https://pantheon.io/ -// Submitted by Gary Dylina -pantheonsite.io -gotpantheon.com - -// Peplink | Pepwave : http://peplink.com/ -// Submitted by Steve Leung -mypep.link - -// Perspecta : https://perspecta.com/ -// Submitted by Kenneth Van Alstyne -perspecta.cloud - -// PE Ulyanov Kirill Sergeevich : https://airy.host -// Submitted by Kirill Ulyanov -lk3.ru - -// Planet-Work : https://www.planet-work.com/ -// Submitted by Frédéric VANNIÈRE -on-web.fr - -// Platform.sh : https://platform.sh -// Submitted by Nikola Kotur -bc.platform.sh -ent.platform.sh -eu.platform.sh -us.platform.sh -*.platformsh.site -*.tst.site - -// Platter: https://platter.dev -// Submitted by Patrick Flor -platter-app.com -platter-app.dev -platterp.us - -// Plesk : https://www.plesk.com/ -// Submitted by Anton Akhtyamov -pdns.page -plesk.page -pleskns.com - -// Port53 : https://port53.io/ -// Submitted by Maximilian Schieder -dyn53.io - -// Porter : https://porter.run/ -// Submitted by Rudraksh MK -onporter.run - -// Positive Codes Technology Company : http://co.bn/faq.html -// Submitted by Zulfais -co.bn - -// Postman, Inc : https://postman.com -// Submitted by Rahul Dhawan -postman-echo.com -pstmn.io -mock.pstmn.io -httpbin.org - -//prequalifyme.today : https://prequalifyme.today -//Submitted by DeepakTiwari deepak@ivylead.io -prequalifyme.today - -// prgmr.com : https://prgmr.com/ -// Submitted by Sarah Newman -xen.prgmr.com - -// priv.at : http://www.nic.priv.at/ -// Submitted by registry -priv.at - -// privacytools.io : https://www.privacytools.io/ -// Submitted by Jonah Aragon -prvcy.page - -// Protocol Labs : https://protocol.ai/ -// Submitted by Michael Burns -*.dweb.link - -// Protonet GmbH : http://protonet.io -// Submitted by Martin Meier -protonet.io - -// Publication Presse Communication SARL : https://ppcom.fr -// Submitted by Yaacov Akiba Slama -chirurgiens-dentistes-en-france.fr -byen.site - -// pubtls.org: https://www.pubtls.org -// Submitted by Kor Nielsen -pubtls.org - -// PythonAnywhere LLP: https://www.pythonanywhere.com -// Submitted by Giles Thomas -pythonanywhere.com -eu.pythonanywhere.com - -// QOTO, Org. -// Submitted by Jeffrey Phillips Freeman -qoto.io - -// Qualifio : https://qualifio.com/ -// Submitted by Xavier De Cock -qualifioapp.com - -// Quality Unit: https://qualityunit.com -// Submitted by Vasyl Tsalko -ladesk.com - -// QuickBackend: https://www.quickbackend.com -// Submitted by Dani Biro -qbuser.com - -// Rad Web Hosting: https://radwebhosting.com -// Submitted by Scott Claeys -cloudsite.builders - -// Redgate Software: https://red-gate.com -// Submitted by Andrew Farries -instances.spawn.cc - -// Redstar Consultants : https://www.redstarconsultants.com/ -// Submitted by Jons Slemmer -instantcloud.cn - -// Russian Academy of Sciences -// Submitted by Tech Support -ras.ru - -// QA2 -// Submitted by Daniel Dent (https://www.danieldent.com/) -qa2.com - -// QCX -// Submitted by Cassandra Beelen -qcx.io -*.sys.qcx.io - -// QNAP System Inc : https://www.qnap.com -// Submitted by Nick Chang -dev-myqnapcloud.com -alpha-myqnapcloud.com -myqnapcloud.com - -// Quip : https://quip.com -// Submitted by Patrick Linehan -*.quipelements.com - -// Qutheory LLC : http://qutheory.io -// Submitted by Jonas Schwartz -vapor.cloud -vaporcloud.io - -// Rackmaze LLC : https://www.rackmaze.com -// Submitted by Kirill Pertsev -rackmaze.com -rackmaze.net - -// Rakuten Games, Inc : https://dev.viberplay.io -// Submitted by Joshua Zhang -g.vbrplsbx.io - -// Rancher Labs, Inc : https://rancher.com -// Submitted by Vincent Fiduccia -*.on-k3s.io -*.on-rancher.cloud -*.on-rio.io - -// Read The Docs, Inc : https://www.readthedocs.org -// Submitted by David Fischer -readthedocs.io - -// Red Hat, Inc. OpenShift : https://openshift.redhat.com/ -// Submitted by Tim Kramer -rhcloud.com - -// Render : https://render.com -// Submitted by Anurag Goel -app.render.com -onrender.com - -// Repl.it : https://repl.it -// Submitted by Lincoln Bergeson -firewalledreplit.co -id.firewalledreplit.co -repl.co -id.repl.co -repl.run - -// Resin.io : https://resin.io -// Submitted by Tim Perry -resindevice.io -devices.resinstaging.io - -// RethinkDB : https://www.rethinkdb.com/ -// Submitted by Chris Kastorff -hzc.io - -// Revitalised Limited : http://www.revitalised.co.uk -// Submitted by Jack Price -wellbeingzone.eu -wellbeingzone.co.uk - -// Rico Developments Limited : https://adimo.co -// Submitted by Colin Brown -adimo.co.uk - -// Riseup Networks : https://riseup.net -// Submitted by Micah Anderson -itcouldbewor.se - -// Rochester Institute of Technology : http://www.rit.edu/ -// Submitted by Jennifer Herting -git-pages.rit.edu - -// Rocky Enterprise Software Foundation : https://resf.org -// Submitted by Neil Hanlon -rocky.page - -// Rusnames Limited: http://rusnames.ru/ -// Submitted by Sergey Zotov -биз.рус -ком.рус -крым.рус -мир.рус -мск.рус -орг.рус -самара.рус -сочи.рус -спб.рус -я.рус - -// SAKURA Internet Inc. : https://www.sakura.ad.jp/ -// Submitted by Internet Service Department -180r.com -dojin.com -sakuratan.com -sakuraweb.com -x0.com -2-d.jp -bona.jp -crap.jp -daynight.jp -eek.jp -flop.jp -halfmoon.jp -jeez.jp -matrix.jp -mimoza.jp -ivory.ne.jp -mail-box.ne.jp -mints.ne.jp -mokuren.ne.jp -opal.ne.jp -sakura.ne.jp -sumomo.ne.jp -topaz.ne.jp -netgamers.jp -nyanta.jp -o0o0.jp -rdy.jp -rgr.jp -rulez.jp -s3.isk01.sakurastorage.jp -s3.isk02.sakurastorage.jp -saloon.jp -sblo.jp -skr.jp -tank.jp -uh-oh.jp -undo.jp -rs.webaccel.jp -user.webaccel.jp -websozai.jp -xii.jp -squares.net -jpn.org -kirara.st -x0.to -from.tv -sakura.tv - -// Salesforce.com, Inc. https://salesforce.com/ -// Submitted by Michael Biven -*.builder.code.com -*.dev-builder.code.com -*.stg-builder.code.com - -// Sandstorm Development Group, Inc. : https://sandcats.io/ -// Submitted by Asheesh Laroia -sandcats.io - -// SBE network solutions GmbH : https://www.sbe.de/ -// Submitted by Norman Meilick -logoip.de -logoip.com - -// Scaleway : https://www.scaleway.com/ -// Submitted by Rémy Léone -fr-par-1.baremetal.scw.cloud -fr-par-2.baremetal.scw.cloud -nl-ams-1.baremetal.scw.cloud -fnc.fr-par.scw.cloud -functions.fnc.fr-par.scw.cloud -k8s.fr-par.scw.cloud -nodes.k8s.fr-par.scw.cloud -s3.fr-par.scw.cloud -s3-website.fr-par.scw.cloud -whm.fr-par.scw.cloud -priv.instances.scw.cloud -pub.instances.scw.cloud -k8s.scw.cloud -k8s.nl-ams.scw.cloud -nodes.k8s.nl-ams.scw.cloud -s3.nl-ams.scw.cloud -s3-website.nl-ams.scw.cloud -whm.nl-ams.scw.cloud -k8s.pl-waw.scw.cloud -nodes.k8s.pl-waw.scw.cloud -s3.pl-waw.scw.cloud -s3-website.pl-waw.scw.cloud -scalebook.scw.cloud -smartlabeling.scw.cloud -dedibox.fr - -// schokokeks.org GbR : https://schokokeks.org/ -// Submitted by Hanno Böck -schokokeks.net - -// Scottish Government: https://www.gov.scot -// Submitted by Martin Ellis -gov.scot -service.gov.scot - -// Scry Security : http://www.scrysec.com -// Submitted by Shante Adam -scrysec.com - -// Securepoint GmbH : https://www.securepoint.de -// Submitted by Erik Anders -firewall-gateway.com -firewall-gateway.de -my-gateway.de -my-router.de -spdns.de -spdns.eu -firewall-gateway.net -my-firewall.org -myfirewall.org -spdns.org - -// Seidat : https://www.seidat.com -// Submitted by Artem Kondratev -seidat.net - -// Sellfy : https://sellfy.com -// Submitted by Yuriy Romadin -sellfy.store - -// Senseering GmbH : https://www.senseering.de -// Submitted by Felix Mönckemeyer -senseering.net - -// Sendmsg: https://www.sendmsg.co.il -// Submitted by Assaf Stern -minisite.ms - -// Service Magnet : https://myservicemagnet.com -// Submitted by Dave Sanders -magnet.page - -// Service Online LLC : http://drs.ua/ -// Submitted by Serhii Bulakh -biz.ua -co.ua -pp.ua - -// Shift Crypto AG : https://shiftcrypto.ch -// Submitted by alex -shiftcrypto.dev -shiftcrypto.io - -// ShiftEdit : https://shiftedit.net/ -// Submitted by Adam Jimenez -shiftedit.io - -// Shopblocks : http://www.shopblocks.com/ -// Submitted by Alex Bowers -myshopblocks.com - -// Shopify : https://www.shopify.com -// Submitted by Alex Richter -myshopify.com - -// Shopit : https://www.shopitcommerce.com/ -// Submitted by Craig McMahon -shopitsite.com - -// shopware AG : https://shopware.com -// Submitted by Jens Küper -shopware.store - -// Siemens Mobility GmbH -// Submitted by Oliver Graebner -mo-siemens.io - -// SinaAppEngine : http://sae.sina.com.cn/ -// Submitted by SinaAppEngine -1kapp.com -appchizi.com -applinzi.com -sinaapp.com -vipsinaapp.com - -// Siteleaf : https://www.siteleaf.com/ -// Submitted by Skylar Challand -siteleaf.net - -// Skyhat : http://www.skyhat.io -// Submitted by Shante Adam -bounty-full.com -alpha.bounty-full.com -beta.bounty-full.com - -// Small Technology Foundation : https://small-tech.org -// Submitted by Aral Balkan -small-web.org - -// Smoove.io : https://www.smoove.io/ -// Submitted by Dan Kozak -vp4.me - -// Snowflake Inc : https://www.snowflake.com/ -// Submitted by Faith Olapade -snowflake.app -privatelink.snowflake.app -streamlit.app -streamlitapp.com - -// Snowplow Analytics : https://snowplowanalytics.com/ -// Submitted by Ian Streeter -try-snowplow.com - -// SourceHut : https://sourcehut.org -// Submitted by Drew DeVault -srht.site - -// Stackhero : https://www.stackhero.io -// Submitted by Adrien Gillon -stackhero-network.com - -// Staclar : https://staclar.com -// Submitted by Q Misell -musician.io -// Submitted by Matthias Merkel -novecore.site - -// staticland : https://static.land -// Submitted by Seth Vincent -static.land -dev.static.land -sites.static.land - -// Storebase : https://www.storebase.io -// Submitted by Tony Schirmer -storebase.store - -// Strategic System Consulting (eApps Hosting): https://www.eapps.com/ -// Submitted by Alex Oancea -vps-host.net -atl.jelastic.vps-host.net -njs.jelastic.vps-host.net -ric.jelastic.vps-host.net - -// Sony Interactive Entertainment LLC : https://sie.com/ -// Submitted by David Coles -playstation-cloud.com - -// SourceLair PC : https://www.sourcelair.com -// Submitted by Antonis Kalipetis -apps.lair.io -*.stolos.io - -// SpaceKit : https://www.spacekit.io/ -// Submitted by Reza Akhavan -spacekit.io - -// SpeedPartner GmbH: https://www.speedpartner.de/ -// Submitted by Stefan Neufeind -customer.speedpartner.de - -// Spreadshop (sprd.net AG) : https://www.spreadshop.com/ -// Submitted by Martin Breest -myspreadshop.at -myspreadshop.com.au -myspreadshop.be -myspreadshop.ca -myspreadshop.ch -myspreadshop.com -myspreadshop.de -myspreadshop.dk -myspreadshop.es -myspreadshop.fi -myspreadshop.fr -myspreadshop.ie -myspreadshop.it -myspreadshop.net -myspreadshop.nl -myspreadshop.no -myspreadshop.pl -myspreadshop.se -myspreadshop.co.uk - -// Standard Library : https://stdlib.com -// Submitted by Jacob Lee -api.stdlib.com - -// Storj Labs Inc. : https://storj.io/ -// Submitted by Philip Hutchins -storj.farm - -// Studenten Net Twente : http://www.snt.utwente.nl/ -// Submitted by Silke Hofstra -utwente.io - -// Student-Run Computing Facility : https://www.srcf.net/ -// Submitted by Edwin Balani -soc.srcf.net -user.srcf.net - -// Sub 6 Limited: http://www.sub6.com -// Submitted by Dan Miller -temp-dns.com - -// Supabase : https://supabase.io -// Submitted by Inian Parameshwaran -supabase.co -supabase.in -supabase.net -su.paba.se - -// Symfony, SAS : https://symfony.com/ -// Submitted by Fabien Potencier -*.s5y.io -*.sensiosite.cloud - -// Syncloud : https://syncloud.org -// Submitted by Boris Rybalkin -syncloud.it - -// Synology, Inc. : https://www.synology.com/ -// Submitted by Rony Weng -dscloud.biz -direct.quickconnect.cn -dsmynas.com -familyds.com -diskstation.me -dscloud.me -i234.me -myds.me -synology.me -dscloud.mobi -dsmynas.net -familyds.net -dsmynas.org -familyds.org -vpnplus.to -direct.quickconnect.to - -// Tabit Technologies Ltd. : https://tabit.cloud/ -// Submitted by Oren Agiv -tabitorder.co.il -mytabit.co.il -mytabit.com - -// TAIFUN Software AG : http://taifun-software.de -// Submitted by Bjoern Henke -taifun-dns.de - -// Tailscale Inc. : https://www.tailscale.com -// Submitted by David Anderson -beta.tailscale.net -ts.net - -// TASK geographical domains (www.task.gda.pl/uslugi/dns) -gda.pl -gdansk.pl -gdynia.pl -med.pl -sopot.pl - -// team.blue https://team.blue -// Submitted by Cedric Dubois -site.tb-hosting.com - -// Teckids e.V. : https://www.teckids.org -// Submitted by Dominik George -edugit.io -s3.teckids.org - -// Telebit : https://telebit.cloud -// Submitted by AJ ONeal -telebit.app -telebit.io -*.telebit.xyz - -// Thingdust AG : https://thingdust.com/ -// Submitted by Adrian Imboden -*.firenet.ch -*.svc.firenet.ch -reservd.com -thingdustdata.com -cust.dev.thingdust.io -cust.disrec.thingdust.io -cust.prod.thingdust.io -cust.testing.thingdust.io -reservd.dev.thingdust.io -reservd.disrec.thingdust.io -reservd.testing.thingdust.io - -// ticket i/O GmbH : https://ticket.io -// Submitted by Christian Franke -tickets.io - -// Tlon.io : https://tlon.io -// Submitted by Mark Staarink -arvo.network -azimuth.network -tlon.network - -// Tor Project, Inc. : https://torproject.org -// Submitted by Antoine Beaupré -bloxcms.com -townnews-staging.com - -// TrafficPlex GmbH : https://www.trafficplex.de/ -// Submitted by Phillipp Röll -12hp.at -2ix.at -4lima.at -lima-city.at -12hp.ch -2ix.ch -4lima.ch -lima-city.ch -trafficplex.cloud -de.cool -12hp.de -2ix.de -4lima.de -lima-city.de -1337.pictures -clan.rip -lima-city.rocks -webspace.rocks -lima.zone - -// TransIP : https://www.transip.nl -// Submitted by Rory Breuk -*.transurl.be -*.transurl.eu -*.transurl.nl - -// TransIP: https://www.transip.nl -// Submitted by Cedric Dubois -site.transip.me - -// TuxFamily : http://tuxfamily.org -// Submitted by TuxFamily administrators -tuxfamily.org - -// TwoDNS : https://www.twodns.de/ -// Submitted by TwoDNS-Support -dd-dns.de -diskstation.eu -diskstation.org -dray-dns.de -draydns.de -dyn-vpn.de -dynvpn.de -mein-vigor.de -my-vigor.de -my-wan.de -syno-ds.de -synology-diskstation.de -synology-ds.de - -// Typedream : https://typedream.com -// Submitted by Putri Karunia -typedream.app - -// Typeform : https://www.typeform.com -// Submitted by Sergi Ferriz -pro.typeform.com - -// Uberspace : https://uberspace.de -// Submitted by Moritz Werner -uber.space -*.uberspace.de - -// UDR Limited : http://www.udr.hk.com -// Submitted by registry -hk.com -hk.org -ltd.hk -inc.hk - -// UK Intis Telecom LTD : https://it.com -// Submitted by ITComdomains -it.com - -// UNIVERSAL DOMAIN REGISTRY : https://www.udr.org.yt/ -// see also: whois -h whois.udr.org.yt help -// Submitted by Atanunu Igbunuroghene -name.pm -sch.tf -biz.wf -sch.wf -org.yt - -// United Gameserver GmbH : https://united-gameserver.de -// Submitted by Stefan Schwarz -virtualuser.de -virtual-user.de - -// Upli : https://upli.io -// Submitted by Lenny Bakkalian -upli.io - -// urown.net : https://urown.net -// Submitted by Hostmaster -urown.cloud -dnsupdate.info - -// .US -// Submitted by Ed Moore -lib.de.us - -// VeryPositive SIA : http://very.lv -// Submitted by Danko Aleksejevs -2038.io - -// Vercel, Inc : https://vercel.com/ -// Submitted by Connor Davis -vercel.app -vercel.dev -now.sh - -// Viprinet Europe GmbH : http://www.viprinet.com -// Submitted by Simon Kissel -router.management - -// Virtual-Info : https://www.virtual-info.info/ -// Submitted by Adnan RIHAN -v-info.info - -// Voorloper.com: https://voorloper.com -// Submitted by Nathan van Bakel -voorloper.cloud - -// Voxel.sh DNS : https://voxel.sh/dns/ -// Submitted by Mia Rehlinger -neko.am -nyaa.am -be.ax -cat.ax -es.ax -eu.ax -gg.ax -mc.ax -us.ax -xy.ax -nl.ci -xx.gl -app.gp -blog.gt -de.gt -to.gt -be.gy -cc.hn -blog.kg -io.kg -jp.kg -tv.kg -uk.kg -us.kg -de.ls -at.md -de.md -jp.md -to.md -indie.porn -vxl.sh -ch.tc -me.tc -we.tc -nyan.to -at.vg -blog.vu -dev.vu -me.vu - -// V.UA Domain Administrator : https://domain.v.ua/ -// Submitted by Serhii Rostilo -v.ua - -// Vultr Objects : https://www.vultr.com/products/object-storage/ -// Submitted by Niels Maumenee -*.vultrobjects.com - -// Waffle Computer Inc., Ltd. : https://docs.waffleinfo.com -// Submitted by Masayuki Note -wafflecell.com - -// WebHare bv: https://www.webhare.com/ -// Submitted by Arnold Hendriks -*.webhare.dev - -// WebHotelier Technologies Ltd: https://www.webhotelier.net/ -// Submitted by Apostolos Tsakpinis -reserve-online.net -reserve-online.com -bookonline.app -hotelwithflight.com - -// WeDeploy by Liferay, Inc. : https://www.wedeploy.com -// Submitted by Henrique Vicente -wedeploy.io -wedeploy.me -wedeploy.sh - -// Western Digital Technologies, Inc : https://www.wdc.com -// Submitted by Jung Jin -remotewd.com - -// WIARD Enterprises : https://wiardweb.com -// Submitted by Kidd Hustle -pages.wiardweb.com - -// Wikimedia Labs : https://wikitech.wikimedia.org -// Submitted by Arturo Borrero Gonzalez -wmflabs.org -toolforge.org -wmcloud.org - -// WISP : https://wisp.gg -// Submitted by Stepan Fedotov -panel.gg -daemon.panel.gg - -// Wizard Zines : https://wizardzines.com -// Submitted by Julia Evans -messwithdns.com - -// WoltLab GmbH : https://www.woltlab.com -// Submitted by Tim Düsterhus -woltlab-demo.com -myforum.community -community-pro.de -diskussionsbereich.de -community-pro.net -meinforum.net - -// Woods Valldata : https://www.woodsvalldata.co.uk/ -// Submitted by Chris Whittle -affinitylottery.org.uk -raffleentry.org.uk -weeklylottery.org.uk - -// WP Engine : https://wpengine.com/ -// Submitted by Michael Smith -// Submitted by Brandon DuRette -wpenginepowered.com -js.wpenginepowered.com - -// Wix.com, Inc. : https://www.wix.com -// Submitted by Shahar Talmi -wixsite.com -editorx.io - -// XenonCloud GbR: https://xenoncloud.net -// Submitted by Julian Uphoff -half.host - -// XnBay Technology : http://www.xnbay.com/ -// Submitted by XnBay Developer -xnbay.com -u2.xnbay.com -u2-local.xnbay.com - -// XS4ALL Internet bv : https://www.xs4all.nl/ -// Submitted by Daniel Mostertman -cistron.nl -demon.nl -xs4all.space - -// Yandex.Cloud LLC: https://cloud.yandex.com -// Submitted by Alexander Lodin -yandexcloud.net -storage.yandexcloud.net -website.yandexcloud.net - -// YesCourse Pty Ltd : https://yescourse.com -// Submitted by Atul Bhouraskar -official.academy - -// Yola : https://www.yola.com/ -// Submitted by Stefano Rivera -yolasite.com - -// Yombo : https://yombo.net -// Submitted by Mitch Schwenk -ybo.faith -yombo.me -homelink.one -ybo.party -ybo.review -ybo.science -ybo.trade - -// Yunohost : https://yunohost.org -// Submitted by Valentin Grimaud -ynh.fr -nohost.me -noho.st - -// ZaNiC : http://www.za.net/ -// Submitted by registry -za.net -za.org - -// Zine EOOD : https://zine.bg/ -// Submitted by Martin Angelov -bss.design - -// Zitcom A/S : https://www.zitcom.dk -// Submitted by Emil Stahl -basicserver.io -virtualserver.io -enterprisecloud.nu - -// ===END PRIVATE DOMAINS=== From 3419934410c7f0ec518b144414aaf14558c70f5d Mon Sep 17 00:00:00 2001 From: Max Ustinov Date: Mon, 10 Aug 2026 22:21:27 -0700 Subject: [PATCH 05/13] Fix Docker container hanging after one-shot commands Every authentication branch in the entrypoint ended with an unconditional `sleep infinity`, so a container invoked with a one-shot command printed its result and then never exited. That made the image unusable from scripts, since the command's exit status never propagated, and there was no way to opt out. Only failing commands exited, and then only incidentally, via `set -e`. Replace the eight unconditional `sleep infinity` calls with a single lifecycle decision: stay resident only for service mode, for an invocation with no command, or when KEEPER_KEEP_ALIVE is set; otherwise exit with the wrapped command's status. Three further problems in the same lifecycle code: - The container ignored SIGTERM. Bash defers trap handlers while a foreground child runs, so `sleep infinity` kept the existing EXIT/INT/TERM trap from ever firing: `docker stop` waited out the full grace period and then SIGKILLed, leaving the KSM config monitor uncleaned. Idle via a backgrounded sleep and `wait`, with explicit TERM/INT handlers. - Command arguments were flattened into a single string and re-split by word splitting, so any argument containing spaces (record titles, notes, search queries) reached Commander as several arguments. Carry them in an array. - The KSM one-shot path no longer starts the perpetual config monitor. It uploads config.json once so refreshed device state persists, then exits. Fixes #2264 --- docker-entrypoint.sh | 257 +++++++++++++++++++++++++++++++------------ 1 file changed, 185 insertions(+), 72 deletions(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 91ffad491..fddda979d 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -27,32 +27,39 @@ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" } -# Function to run keeper service with intelligent lifecycle management +# Function to run keeper service with intelligent lifecycle management. +# Usage: run_keeper_service [args...] +# Arguments are passed through as an array so values containing spaces survive. run_keeper_service() { - local config_arg="$1" - local command_args="$2" - + local config_file="$1" + shift + + local config_args=() + if [[ -n "${config_file}" ]]; then + config_args=("--config" "${config_file}") + fi + # Check if command contains service-create - if [[ "$command_args" =~ service-create ]]; then + if [[ "$*" =~ service-create ]]; then log "Service command detected, checking service status..." - + # Get service status local service_status - service_status=$(python3 keeper.py ${config_arg} service-status 2>/dev/null || true) - + service_status=$(python3 keeper.py "${config_args[@]}" service-status 2>/dev/null || true) + if echo "${service_status}" | grep -q "Stopped"; then log "Service exists but is stopped, starting it..." - python3 keeper.py ${config_arg} service-start + python3 keeper.py "${config_args[@]}" service-start elif echo "${service_status}" | grep -q "Running"; then log "Service is already running, no action needed." else log "Service not found, creating new service..." - log "Running: python3 keeper.py ${config_arg} ${command_args}" - python3 keeper.py ${config_arg} ${command_args} + log "Running: python3 keeper.py ${config_args[*]} $*" + python3 keeper.py "${config_args[@]}" "$@" fi else # Not a service command, run as normal - python3 keeper.py ${config_arg} ${command_args} + python3 keeper.py "${config_args[@]}" "$@" fi } @@ -103,23 +110,28 @@ parse_credentials() { done } -# Filter out authentication arguments and return remaining command arguments +# Filter out authentication arguments, leaving the actual Commander command in +# the global COMMAND_ARGS array. An array rather than a string so that arguments +# containing spaces -- record titles, notes, search queries -- reach Commander +# as single arguments instead of being re-split by word splitting. filter_args() { - local filtered_args=() - + COMMAND_ARGS=() + while [[ $# -gt 0 ]]; do case $1 in --user|--password|--server|--ksm-config|--ksm-token|--record) - shift 2 # Skip argument and its value + # Skip the flag and its value, tolerating a missing value. + shift + if [[ $# -gt 0 ]]; then + shift + fi ;; *) - filtered_args+=("$1") + COMMAND_ARGS+=("$1") shift ;; esac done - - echo "${filtered_args[@]}" } @@ -221,6 +233,36 @@ download_config_from_ksm() { log "Config.json downloaded successfully from KSM record" } +# Upload config.json back to the KSM record once, without starting a monitor. +# Used after a one-shot command so refreshed device/session state is persisted +# before the container exits. Best effort: a failed upload must not mask the +# status of the command the user actually asked for. +upload_config_to_ksm() { + local ksm_config_path="$1" + local ksm_token="$2" + local record_uid="$3" + + if [[ ! -f "${CONFIG_FILE}" ]]; then + return 0 + fi + + local helper_args=("upload" "--record-uid" "${record_uid}" \ + "--config-file" "${CONFIG_FILE}") + + if [[ -n "${ksm_config_path}" ]]; then + helper_args+=("--ksm-config" "${ksm_config_path}") + elif [[ -n "${ksm_token}" ]]; then + helper_args+=("--ksm-token" "${ksm_token}") + else + return 0 + fi + + log "Syncing config.json back to KSM record: ${record_uid}" + if ! python3 docker_ksm_utility.py "${helper_args[@]}"; then + log "WARNING: Failed to sync config back to KSM record" + fi +} + # Start config.json monitoring and upload changes start_config_monitor() { local ksm_config_path="$1" @@ -275,6 +317,9 @@ stop_config_monitor() { # CLEANUP AND SIGNAL HANDLING # ============================================================================= +# PID of the idle process used by keep-alive mode, so signal handling can end it. +KEEP_ALIVE_PID="" + # Handle cleanup on exit cleanup_on_exit() { log "Performing cleanup on exit..." @@ -282,8 +327,93 @@ cleanup_on_exit() { log "Cleanup completed" } -# Set up exit trap for cleanup -trap cleanup_on_exit EXIT INT TERM +# Handle SIGTERM/SIGINT (e.g. `docker stop`) by ending the keep-alive wait and +# exiting, which runs the EXIT trap and therefore the cleanup above. +on_terminate() { + local signum="$1" + + log "Received signal, shutting down..." + if [[ -n "${KEEP_ALIVE_PID}" ]]; then + kill "${KEEP_ALIVE_PID}" 2>/dev/null || true + KEEP_ALIVE_PID="" + fi + + # Conventional shell status for death by signal. + exit $((128 + signum)) +} + +# Set up traps: cleanup always runs on exit, signals shut down deliberately. +trap cleanup_on_exit EXIT +trap 'on_terminate 15' TERM +trap 'on_terminate 2' INT + + +# ============================================================================= +# CONTAINER LIFECYCLE +# ============================================================================= + +# Decide whether the container should stay resident once setup is done. +# +# Persistent modes have to stay up: service mode leaves Commander running as a +# background daemon, and a bare `docker run` with no command is a request for a +# live container to exec into. A one-shot command is the opposite -- it must +# exit with its own status so the container behaves like the CLI it wraps and +# can be used in scripts and pipelines. Set KEEPER_KEEP_ALIVE=true to force the +# container to stay up regardless. +should_keep_alive() { + local flag + flag=$(printf '%s' "${KEEPER_KEEP_ALIVE:-}" | tr '[:upper:]' '[:lower:]') + case "${flag}" in + 1|true|yes) + return 0 + ;; + esac + + # No command given -- nothing to finish, so keep the container available. + if [[ ${#COMMAND_ARGS[@]} -eq 0 ]]; then + return 0 + fi + + # Service mode runs Commander as a daemon inside the container. + if [[ "${COMMAND_ARGS[*]}" =~ (service-create|service-start) ]]; then + return 0 + fi + + return 1 +} + +# Run the requested command (if any) and return its exit status. Uses +# `|| status=$?` so that `set -e` does not abort before we can report it. +run_command_args() { + local config_file="$1" + local status=0 + + if [[ ${#COMMAND_ARGS[@]} -gt 0 ]]; then + run_keeper_service "${config_file}" "${COMMAND_ARGS[@]}" || status=$? + fi + + return "${status}" +} + +# Final disposition: hold the container open for persistent modes, otherwise +# exit with the wrapped command's status. +finish() { + local status="${1:-0}" + + if should_keep_alive; then + log "Keeping container alive..." + # Idle in the background and `wait` rather than running `sleep infinity` + # in the foreground: bash defers trap handlers until a foreground child + # finishes, so a foreground sleep would make the container ignore + # SIGTERM entirely and force `docker stop` to fall back to SIGKILL + # without ever running cleanup. + sleep infinity & + KEEP_ALIVE_PID=$! + wait "${KEEP_ALIVE_PID}" || true + fi + + exit "${status}" +} # ============================================================================= @@ -331,40 +461,35 @@ if [[ (-n "${KSM_CONFIG}" || -n "${KSM_TOKEN}") && -n "${RECORD}" ]]; then # Download config.json from KSM record download_config_from_ksm "${KSM_CONFIG}" "${KSM_TOKEN}" "${RECORD}" - - # Start monitoring for config.json changes to upload back to KSM - start_config_monitor "${KSM_CONFIG}" "${KSM_TOKEN}" "${RECORD}" - + # Filter out KSM arguments from command args - COMMAND_ARGS=$(filter_args "$@") - - # Execute commands or keep container alive - if [[ -z "${COMMAND_ARGS}" ]]; then - log "No command arguments provided, keeping container alive..." - sleep infinity - else - # Run the service command with downloaded config file - run_keeper_service "--config ${CONFIG_FILE}" "${COMMAND_ARGS}" - log "Keeping container alive..." - sleep infinity + filter_args "$@" + + if should_keep_alive; then + # Persistent run: watch config.json and push changes back to the record + # for as long as the container lives. + start_config_monitor "${KSM_CONFIG}" "${KSM_TOKEN}" "${RECORD}" + status=0 + run_command_args "${CONFIG_FILE}" || status=$? + finish "${status}" fi + + # One-shot run: no monitor needed. Sync the config back once so refreshed + # device/session state is preserved, then exit with the command's status. + status=0 + run_command_args "${CONFIG_FILE}" || status=$? + upload_config_to_ksm "${KSM_CONFIG}" "${KSM_TOKEN}" "${RECORD}" + exit "${status}" # Check if config.json is mounted or available elif [[ -f "${CONFIG_FILE}" ]]; then log "Config file found at ${CONFIG_FILE}, using config-based authentication" - + # Filter out authentication arguments, keep the rest - COMMAND_ARGS=$(filter_args "$@") - - # Execute commands or keep container alive - if [[ -z "${COMMAND_ARGS}" ]]; then - log "No command arguments provided, keeping container alive..." - sleep infinity - else - # Run the service command with config file - run_keeper_service "--config ${CONFIG_FILE}" "${COMMAND_ARGS}" - log "Keeping container alive..." - sleep infinity - fi + filter_args "$@" + + status=0 + run_command_args "${CONFIG_FILE}" || status=$? + finish "${status}" # Check if user/password authentication is provided elif [[ -n "${USER}" && -n "${PASSWORD}" ]]; then log "No config file found, using user/password authentication" @@ -381,33 +506,21 @@ elif [[ -n "${USER}" && -n "${PASSWORD}" ]]; then setup_device "${USER}" "${PASSWORD}" "${SERVER}" # Filter out authentication arguments, keep the rest - COMMAND_ARGS=$(filter_args "$@") + filter_args "$@" - # Execute commands or keep container alive - if [[ -z "${COMMAND_ARGS}" ]]; then - log "Keeping container alive..." - sleep infinity - else - # Run the service command without credentials (device is now registered) - run_keeper_service "" "${COMMAND_ARGS}" - log "Keeping container alive..." - sleep infinity - fi + # Run without credentials -- the device is registered at this point. + status=0 + run_command_args "" || status=$? + finish "${status}" # Fallback: no authentication provided else log "No config file found and no user/password provided" - + # Filter out authentication arguments, keep the rest - COMMAND_ARGS=$(filter_args "$@") + filter_args "$@" - # Execute commands or keep container alive - if [[ -z "${COMMAND_ARGS}" ]]; then - log "Keeping container alive..." - sleep infinity - else - # Run the command directly without any authentication setup - run_keeper_service "" "${COMMAND_ARGS}" - log "Keeping container alive..." - sleep infinity - fi + # Run the command directly without any authentication setup + status=0 + run_command_args "" || status=$? + finish "${status}" fi \ No newline at end of file From e449301029e21c0cae049f067a2a4c528661d697 Mon Sep 17 00:00:00 2001 From: John Walstra Date: Wed, 29 Jul 2026 15:09:05 -0500 Subject: [PATCH 06/13] Change service command to accept service names. (PG-367) --- .../commands/discover/result_process.py | 5 +- .../commands/pam_service/__init__.py | 48 +- keepercommander/commands/pam_service/add.py | 35 +- keepercommander/commands/pam_service/list.py | 202 ++++---- keepercommander/commands/pam_service/map.py | 1 + .../commands/pam_service/remove.py | 35 +- .../discovery_common/__version__.py | 2 +- keepercommander/discovery_common/process.py | 53 +- keepercommander/discovery_common/rm_types.py | 107 ++++ keepercommander/discovery_common/types.py | 83 ++- .../discovery_common/user_service.py | 473 ++++++++++++------ 11 files changed, 725 insertions(+), 319 deletions(-) diff --git a/keepercommander/commands/discover/result_process.py b/keepercommander/commands/discover/result_process.py index d221aab20..e7e5d474d 100644 --- a/keepercommander/commands/discover/result_process.py +++ b/keepercommander/commands/discover/result_process.py @@ -34,7 +34,6 @@ from ...params import KeeperParams from ...vault import TypedRecord, KeeperRecord from ...keeper_dag.vertex import DAGVertex - from ...discovery_common.record_link import RecordLink def _h(value: str) -> str: @@ -1662,6 +1661,7 @@ def execute(self, params: KeeperParams, **kwargs): process = Process( record=configuration_record, job_id=job_item.job_id, + record_lookup_func=self._record_lookup, params=params, logger=logging, debug_level=debug_level, @@ -1674,9 +1674,6 @@ def execute(self, params: KeeperParams, **kwargs): try: results = process.run( - # This method can get a record using the record UID - record_lookup_func=self._record_lookup, - # Prompt user the about adding records prompt_func=self._prompt, diff --git a/keepercommander/commands/pam_service/__init__.py b/keepercommander/commands/pam_service/__init__.py index b41fd4018..fe732764f 100644 --- a/keepercommander/commands/pam_service/__init__.py +++ b/keepercommander/commands/pam_service/__init__.py @@ -1,10 +1,13 @@ from __future__ import annotations +from ...discovery_common.types import NormalizedRecord, RecordField from ...utils import value_to_boolean +from ... import vault import os -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from ...params import KeeperParams + from ...vault import TypedRecord from ...keeper_dag.connection import ConnectionBase @@ -14,4 +17,45 @@ def get_connection(params: KeeperParams) -> ConnectionBase: return CommanderConnection(params=params) else: from ...keeper_dag.connection.local import Connection as LocalConnection - return LocalConnection() \ No newline at end of file + return LocalConnection() + + +def record_lookup(record_uid: str, + context: Any | None = None, + **kwargs) -> NormalizedRecord | None: + + """ + Get the record from the Vault, normalize it, and return it. + + Since common code is using this method we want to flatten/abstract the KeeperRecord/TypedRecord. + """ + + params = context.get("params") + record = vault.TypedRecord.load(params, record_uid) # type: TypedRecord | None + if record is None: + return None + + normalized_record = NormalizedRecord( + record_uid=record.record_uid, + record_type=record.record_type, + record_key_bytes=record.record_key, + title=record.title, + ) + for field in record.fields: + normalized_record.fields.append( + RecordField( + type=field.type, + label=field.label, + value=field.value, + ) + ) + if record.custom is not None: + for field in record.custom: + normalized_record.fields.append( + RecordField( + type=field.type, + label=field.label, + value=field.value, + ) + ) + return normalized_record \ No newline at end of file diff --git a/keepercommander/commands/pam_service/add.py b/keepercommander/commands/pam_service/add.py index 92aa13bee..05c9ec055 100644 --- a/keepercommander/commands/pam_service/add.py +++ b/keepercommander/commands/pam_service/add.py @@ -1,13 +1,14 @@ from __future__ import annotations import argparse import logging +from . import record_lookup from ..discover import PAMGatewayActionDiscoverCommandBase, GatewayContext, MultiConfigurationException, multi_conf_msg from ...display import bcolors from ... import vault from ...discovery_common.user_service import UserService from ...discovery_common.record_link import RecordLink from ...discovery_common.constants import PAM_USER, PAM_MACHINE -from ...discovery_common.types import UserAcl +from ...discovery_common.types import UserAcl, ServiceEnum from ...keeper_dag.types import RefType, EdgeType from ... import __version__ from typing import Optional, TYPE_CHECKING @@ -30,6 +31,11 @@ class PAMActionServiceAddCommand(PAMGatewayActionDiscoverCommandBase): help='The UID of the Windows Machine record') parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', help='The UID of the User record') + parser.add_argument('--type', '-t', required=True, dest='service_type', action='store', + choices=["service", "task", "iis_pool"], + help='Type of service.') + parser.add_argument('--name', '-n', required=True, dest='name', action='store', + help='Name label for reporting.') def get_parser(self): return PAMActionServiceAddCommand.parser @@ -39,6 +45,8 @@ def execute(self, params: KeeperParams, **kwargs): gateway = kwargs.get("gateway", "not_set") machine_uid = kwargs.get("machine_uid", "not_set") user_uid = kwargs.get("user_uid", "not_set") + service_type = kwargs.get("service_type", "unknown") + name = kwargs.get("name", "Unknown") print("") @@ -63,6 +71,7 @@ def execute(self, params: KeeperParams, **kwargs): agent=f"Cmdr/{__version__}", use_per_graph_endpoints=False) user_service = UserService(record=gateway_context.configuration, + record_lookup_func=record_lookup, record_linking=record_link, params=params, fail_on_corrupt=False, @@ -165,18 +174,24 @@ def execute(self, params: KeeperParams, **kwargs): if acl is None: acl = UserAcl.default() - if not hasattr(acl, "controls_services") or not acl.controls_services: - acl.controls_services = True + orig_md5 = acl.md5(user_record.record_key) - # Make sure the machine has a LINK connection to the configuration. - if not root_vertex.has(machine_vertex): - machine_vertex.belongs_to_root(edge_type=EdgeType.LINK) + # Make sure the machine has a LINK connection to the configuration. + if not root_vertex.has(machine_vertex): + machine_vertex.belongs_to_root(edge_type=EdgeType.LINK) - # Add our new ACL edge between the machine and the user. - user_service.set_acl(resource_uid=machine_vertex.uid, - user_uid=user_vertex.uid, - acl=acl) + user_service.add_service_name(acl=acl, + service_type=ServiceEnum.find_enum(service_type), + service_name=name, + record_key_bytes=user_record.record_key) + # Add our new ACL edge between the machine and the user. + user_service.set_acl(resource_uid=machine_vertex.uid, + user_uid=user_vertex.uid, + acl=acl) + + # Only save if the ACL changed. + if orig_md5 != acl.md5(user_record.record_key): record_link.save() else: logging.debug("user already set to control services on this machine.") diff --git a/keepercommander/commands/pam_service/list.py b/keepercommander/commands/pam_service/list.py index bdb5e99c2..c74cd4c6e 100644 --- a/keepercommander/commands/pam_service/list.py +++ b/keepercommander/commands/pam_service/list.py @@ -1,13 +1,13 @@ from __future__ import annotations import argparse -import json +from . import record_lookup from ..discover import PAMGatewayActionDiscoverCommandBase, GatewayContext, MultiConfigurationException, multi_conf_msg from ...display import bcolors from ... import vault from ...discovery_common.user_service import UserService from ...discovery_common.record_link import RecordLink from ...discovery_common.constants import PAM_MACHINE -from ...discovery_common.types import UserData, MachineData +from ...discovery_common.types import ServiceEnum, MachineData, UserData from ...keeper_dag import EdgeType from ... import __version__ from typing import Optional, TYPE_CHECKING @@ -27,13 +27,11 @@ class PAMActionServiceListCommand(PAMGatewayActionDiscoverCommandBase): action='store', help='PAM configuration UID, if gateway has multiple.') parser.add_argument('--by-machine', '-m', required=False, dest='do_by_machine', action='store_true', help='List by machine') - parser.add_argument('--format', dest='format', action='store', choices=['table', 'json'], - default='table', help='Output format (table, json)') def get_parser(self): return PAMActionServiceListCommand.parser - def _collect_by_user(self, params: KeeperParams, record_link: RecordLink, user_service: UserService): + def _by_user(self, params: KeeperParams, record_link: RecordLink, user_service: UserService): service_map = {} for resource_vertex in record_link.dag.get_root.has_vertices(edge_type=EdgeType.LINK): @@ -42,10 +40,11 @@ def _collect_by_user(self, params: KeeperParams, record_link: RecordLink, user_s continue resource_active = True - user_data_edge = resource_vertex.get_data() - if user_data_edge is not None: - user_data = user_data_edge.content_as_object(MachineData) - resource_active = not user_data.no_update_services + resource_data_edge = resource_vertex.get_data() + if resource_data_edge is not None: + resource_data_edge = resource_data_edge.content_as_object(MachineData) + if resource_data_edge is not None: + resource_active = not resource_data_edge.no_update_services user_vertices = user_service.get_user_vertices(resource_vertex.uid) if len(user_vertices) > 0: @@ -61,34 +60,74 @@ def _collect_by_user(self, params: KeeperParams, record_link: RecordLink, user_s user_data_edge = user_vertex.get_data() if user_data_edge is not None: user_data = user_data_edge.content_as_object(UserData) - user_active = not user_data.no_update_services + if user_data is not None: + user_active = not user_data.no_update_services if user_record.record_uid not in service_map: service_map[user_record.record_uid] = { "title": user_record.title, - "uid": user_record.record_uid, "active": user_active, "machines": [] } + machine_name = f"{resource_record.title} ({resource_record.record_uid})" + if not resource_active: + machine_name += f" : {bcolors.FAIL}Disabled{bcolors.ENDC}" + + items = [] + if acl.service_names is not None or acl.service_names != "": + for service_name in acl.get_service_names(user_record.record_key): + text = "" + if service_name.type == ServiceEnum.service: + text = "Service" + elif service_name.type == ServiceEnum.task: + text = "Scheduled Task" + elif service_name.type == ServiceEnum.iis_pool: + text = "IIS Pool" + for item in service_name.items: + text += f": {item.name}" + if "Unknown" in item.name: + text += " (from migration)" + elif not item.via_discovery: + text += " (manually set)" + items.append(text) + service_map[user_record.record_uid]["machines"].append({ - "title": resource_record.title, - "uid": resource_record.record_uid, - "active": resource_active, + "name": machine_name, + "items": items }) - return service_map - def _collect_by_machine(self, params: KeeperParams, record_link: RecordLink, user_service: UserService): + print("") + printed_something = False + print(self._h("User Mapping")) + for user_uid in service_map: + user = service_map[user_uid] + printed_something = True + active_text = "" + if not user['active']: + active_text = f" {bcolors.FAIL}Disabled{bcolors.ENDC}" + print(f" {self._b(user['title'])} ({user_uid}){active_text}") + for machine in user["machines"]: + print(f" * {machine['name']}") + for item in machine["items"]: + print(f" - {self._gr(item)}") + print("") + if not printed_something: + print(f" {bcolors.FAIL}There are no service mappings.{bcolors.ENDC}") + + def _by_machine(self, params: KeeperParams, record_link: RecordLink, user_service: UserService): service_map = {} for resource_vertex in record_link.dag.get_root.has_vertices(edge_type=EdgeType.LINK): + resource_record = vault.KeeperRecord.load(params, resource_vertex.uid) # type: Optional[TypedRecord] if resource_record is None or resource_record.record_type != PAM_MACHINE: continue resource_active = True - user_data_edge = resource_vertex.get_data() - if user_data_edge is not None: - user_data = user_data_edge.content_as_object(MachineData) - resource_active = not user_data.no_update_services + resource_data_edge = resource_vertex.get_data() + if resource_data_edge is not None: + resource_data_edge = resource_data_edge.content_as_object(MachineData) + if resource_data_edge is not None: + resource_active = not resource_data_edge.no_update_services user_vertices = user_service.get_user_vertices(resource_vertex.uid) if len(user_vertices) > 0: @@ -104,58 +143,56 @@ def _collect_by_machine(self, params: KeeperParams, record_link: RecordLink, use user_data_edge = user_vertex.get_data() if user_data_edge is not None: user_data = user_data_edge.content_as_object(UserData) - user_active = not user_data.no_update_services + if user_data is not None: + user_active = not user_data.no_update_services if resource_record.record_uid not in service_map: service_map[resource_record.record_uid] = { "title": resource_record.title, - "uid": resource_record.record_uid, "active": resource_active, "users": [] } + user_name = f"{user_record.title} ({user_record.record_uid})" + if not user_active: + user_name += f" : {bcolors.FAIL}Disabled{bcolors.ENDC}" + + items = [] + if acl.service_names is not None or acl.service_names != "": + for service_name in acl.get_service_names(user_record.record_key): + text = "" + if service_name.type == ServiceEnum.service: + text = "Service" + elif service_name.type == ServiceEnum.task: + text = "Scheduled Task" + elif service_name.type == ServiceEnum.iis_pool: + text = "IIS Pool" + for item in service_name.items: + text += f": {item.name}" + if "Unknown" in item.name: + text += " (from migration)" + elif not item.via_discovery: + text += " (manually set)" + items.append(text) + service_map[resource_record.record_uid]["users"].append({ - "title": user_record.title, - "uid": user_record.record_uid, - "active": user_active, + "name": user_name, + "items": items }) - return service_map - def _print_by_user(self, service_map): - print("") - printed_something = False - print(self._h("User Mapping")) - for user_uid in service_map: - user = service_map[user_uid] - printed_something = True - active_text = "" - if not user['active']: - active_text = f" {bcolors.FAIL}Disabled{bcolors.ENDC}" - print(f" {self._b(user['title'])} ({user_uid}){active_text}") - for machine in user["machines"]: - text = f"{machine['title']} ({machine['uid']})" - if not machine['active']: - text += f" : {bcolors.FAIL}Disabled{bcolors.ENDC}" - print(f" * {text}") - print("") - if not printed_something: - print(f" {bcolors.FAIL}There are no service mappings.{bcolors.ENDC}") - - def _print_by_machine(self, service_map): print("") printed_something = False print(self._h("Machine Mapping")) for resource_uid in service_map: - resource = service_map[resource_uid] + user = service_map[resource_uid] printed_something = True active_text = "" - if not resource['active']: + if not user['active']: active_text = f" {bcolors.FAIL}Disabled{bcolors.ENDC}" - print(f" {self._b(resource['title'])} ({resource_uid}){active_text}") - for user in resource["users"]: - text = f"{user['title']} ({user['uid']})" - if not user['active']: - text += f" : {bcolors.FAIL}Disabled{bcolors.ENDC}" - print(f" * {text}") + print(f" {self._b(user['title'])} ({resource_uid}){active_text}") + for user in user["users"]: + print(f" * {user['name']}") + for item in user["items"]: + print(f" - {self._gr(item)}") print("") if not printed_something: print(f" {bcolors.FAIL}There are no service mappings.{bcolors.ENDC}") @@ -163,35 +200,16 @@ def _print_by_machine(self, service_map): def execute(self, params: KeeperParams, **kwargs): gateway = kwargs.get("gateway", "none_set") - format_type = kwargs.get('format') or 'table' try: gateway_context = GatewayContext.from_gateway(params=params, gateway=gateway, configuration_uid=kwargs.get('configuration_uid')) if gateway_context is None: - message = f'Could not find the gateway configuration for {gateway}.' - if format_type == 'json': - print(json.dumps({'message': message}, indent=2)) - else: - print(f"{bcolors.FAIL}{message}{bcolors.ENDC}") + print(f"{bcolors.FAIL}Could not find the gateway configuration for {gateway}.{bcolors.ENDC}") return except MultiConfigurationException as err: - if format_type == 'json': - configs = [] - for item in (err.items or []): - record = item.get('configuration_record') - if record is not None: - configs.append({ - 'uid': getattr(record, 'record_uid', ''), - 'title': getattr(record, 'title', ''), - }) - print(json.dumps({ - 'message': f'Found multiple configuration records for gateway {gateway}.', - 'configurations': configs, - }, indent=2)) - else: - multi_conf_msg(gateway, err) + multi_conf_msg(gateway, err) return record_link = RecordLink(record=gateway_context.configuration, @@ -202,33 +220,17 @@ def execute(self, params: KeeperParams, **kwargs): # This will trigger the migration. user_service = UserService(record=gateway_context.configuration, + record_lookup_func=record_lookup, record_linking=record_link, params=params, fail_on_corrupt=False, agent=f"Cmdr/{__version__}") - by_machine = bool(kwargs.get("do_by_machine")) - if by_machine: - service_map = self._collect_by_machine(params=params, - record_link=record_link, - user_service=user_service) - else: - service_map = self._collect_by_user(params=params, - record_link=record_link, - user_service=user_service) - - if format_type == 'json': - payload = { - 'gateway': gateway_context.gateway_name, - 'gateway_uid': gateway_context.gateway_uid, - 'configuration_uid': gateway_context.configuration_uid, - 'group_by': 'machine' if by_machine else 'user', - 'mappings': list(service_map.values()), - } - print(json.dumps(payload, indent=2)) - return - - if by_machine: - self._print_by_machine(service_map) + if kwargs.get("do_by_machine"): + self._by_machine(params=params, + record_link=record_link, + user_service=user_service) else: - self._print_by_user(service_map) + self._by_user(params=params, + record_link=record_link, + user_service=user_service) diff --git a/keepercommander/commands/pam_service/map.py b/keepercommander/commands/pam_service/map.py index 599a50c5a..9bc07ad94 100644 --- a/keepercommander/commands/pam_service/map.py +++ b/keepercommander/commands/pam_service/map.py @@ -40,6 +40,7 @@ def __init__(self, inputs: GatewayActionUserServiceMapCommandInputs, conversatio def toJSON(self): return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + class PAMActionUserServiceMapCommand(PAMGatewayActionDiscoverCommandBase): parser = argparse.ArgumentParser(prog='pam action saas config') diff --git a/keepercommander/commands/pam_service/remove.py b/keepercommander/commands/pam_service/remove.py index 3f6f7087c..f66da90e0 100644 --- a/keepercommander/commands/pam_service/remove.py +++ b/keepercommander/commands/pam_service/remove.py @@ -1,10 +1,12 @@ from __future__ import annotations import argparse +from . import record_lookup from ..discover import PAMGatewayActionDiscoverCommandBase, GatewayContext, MultiConfigurationException, multi_conf_msg from ... import vault from ...discovery_common.constants import PAM_USER, PAM_MACHINE from ...discovery_common.user_service import UserService from ...discovery_common.record_link import RecordLink +from ...discovery_common.types import ServiceEnum from ...display import bcolors from ... import __version__ from typing import Optional, TYPE_CHECKING @@ -28,6 +30,15 @@ class PAMActionServiceRemoveCommand(PAMGatewayActionDiscoverCommandBase): parser.add_argument('--user-uid', '-u', required=True, dest='user_uid', action='store', help='The UID of the User record') + parser.add_argument('--type', '-t', required=True, dest='service_type', action='store', + choices=["service", "task", "iis_pool", "all"], + help='Type of service. "all" will clear all') + parser.add_argument('--name', '-n', required=False, dest='name', action='store', + help='Name label for reporting. Exclude will remove all service types.') + + parser.add_argument('--force', required=False, dest='do_force', action='store_true', + help='Force') + def get_parser(self): return PAMActionServiceRemoveCommand.parser @@ -36,6 +47,9 @@ def execute(self, params: KeeperParams, **kwargs): gateway = kwargs.get("gateway", "not_set") machine_uid = kwargs.get("machine_uid", "not_set") user_uid = kwargs.get("user_uid", "not_set") + service_type = kwargs.get("service_type", "unknown") + name = kwargs.get("name", None) + do_force = kwargs.get("do_force", False) print("") @@ -60,6 +74,7 @@ def execute(self, params: KeeperParams, **kwargs): agent=f"Cmdr/{__version__}", use_per_graph_endpoints=False) user_service = UserService(record=gateway_context.configuration, + record_lookup_func=record_lookup, record_linking=record_link, params=params, fail_on_corrupt=False, @@ -100,12 +115,22 @@ def execute(self, params: KeeperParams, **kwargs): return if not hasattr(acl, "controls_services") or acl.controls_services: - acl.controls_services = False - user_service.set_acl(resource_uid=machine_vertex.uid, - user_uid=user_vertex.uid, - acl=acl) - record_link.save() + if (service_type == "all" or name is None) and not do_force: + print(f"{bcolors.WARNING}Require --force parameter to make changes.{bcolors.ENDC}") + return + + orig_md5 = acl.md5(user_record.record_key) + user_service.remove_service_name(acl=acl, + record_key_bytes=user_record.record_key, + service_type=ServiceEnum.find_enum(service_type), + service_name=name) + + if orig_md5 != acl.md5(user_record.record_key): + user_service.set_acl(resource_uid=machine_vertex.uid, + user_uid=user_vertex.uid, + acl=acl) + record_link.save() print( self._gr( diff --git a/keepercommander/discovery_common/__version__.py b/keepercommander/discovery_common/__version__.py index ec5c76229..b46c29a4b 100644 --- a/keepercommander/discovery_common/__version__.py +++ b/keepercommander/discovery_common/__version__.py @@ -1 +1 @@ -__version__ = '1.1.16' +__version__ = '1.1.22' diff --git a/keepercommander/discovery_common/process.py b/keepercommander/discovery_common/process.py index 33f5a5fd4..206dad667 100644 --- a/keepercommander/discovery_common/process.py +++ b/keepercommander/discovery_common/process.py @@ -68,9 +68,17 @@ class Process: # Hard limit for bulk record lists (safety mechanism) BULK_LIST_MAX_SIZE = 50000 - def __init__(self, record: Any, job_id: str, logger: Optional[Any] = None, debug_level: int = 0, **kwargs): + def __init__(self, + record: Any, + job_id: str, + record_lookup_func: Callable, + logger: Optional[Any] = None, + debug_level: int = 0, + **kwargs): + self.job_id = job_id self.record = record + self.record_lookup_func = record_lookup_func env_debug_level = os.environ.get("PROCESS_GS_DEBUG_LEVEL") if env_debug_level is not None: @@ -92,6 +100,7 @@ def __init__(self, record: Any, job_id: str, logger: Optional[Any] = None, debug logger=logger, debug_level=debug_level, record_linking=self.record_link, + record_lookup_func=record_lookup_func, **kwargs) # This is the root UID for all graphs; get it from one of them. @@ -421,7 +430,6 @@ def _directory_exists(self, domain: str, directory_info_func: Callable, context: def _find_directory_user(self, results: DirectoryResult, - record_lookup_func: Callable, context: Any, find_user: Optional[str] = None, find_dn: Optional[str] = None) -> Optional[DirectoryUserResult]: @@ -431,7 +439,7 @@ def _find_directory_user(self, self.logger.debug("search for directory user from vault records") self.logger.debug(f"have {len(results.directory_user_record_uids)} users") for user_record_id in results.directory_user_record_uids: - record = record_lookup_func(record_uid=user_record_id, context=context) # type: NormalizedRecord + record = self.record_lookup_func(user_record_id, context=context) # type: NormalizedRecord if record is not None: found = None self.logger.debug(f"find user {find_user}, dn {find_dn}") @@ -551,7 +559,6 @@ def _find_admin_directory_user(self, domain: str, admin_acl: UserAcl, directory_info_func: Callable, - record_lookup_func: Callable, context: Any, user: Optional[str] = None, dn: Optional[str] = None) -> Optional[str]: @@ -564,7 +571,6 @@ def _find_admin_directory_user(self, if results is not None: # Find the user (clean of domain) or DN in the found directories. directory_user = self._find_directory_user(results=results, - record_lookup_func=record_lookup_func, context=context, find_user=user, find_dn=dn) @@ -603,7 +609,6 @@ def _process_auto_add_level(self, current_vertex: DAGVertex, bulk_add_records: List[BulkRecordAdd], bulk_convert_records: List[BulkRecordConvert], - record_lookup_func: Callable, record_prepare_func: Callable, directory_info_func: Callable, record_cache: dict, @@ -617,7 +622,6 @@ def _process_auto_add_level(self, :param current_vertex: The current/parent discovery vertex. :param bulk_add_records: List of records to be added. :param bulk_convert_records: List of existing records to be covert to this gateway. - :params record_lookup_func: A function to lookup records to see if they exist. :param record_prepare_func: Function to convert content into an unsaved record. :param directory_info_func: Function to lookup directories. :param record_cache: @@ -672,7 +676,7 @@ def _process_auto_add_level(self, # not created a record for yet, however it might have been assigned a record UID from a prior prompt. existing_record = child_content.record_exists - if record_lookup_func is not None: + if self.record_lookup_func is not None: check_the_vault = True for item in bulk_add_records: if item.record_uid == child_content.record_uid: @@ -680,8 +684,8 @@ def _process_auto_add_level(self, check_the_vault = False break if check_the_vault: - existing_record = record_lookup_func(record_uid=child_content.record_uid, - context=context) is not None + existing_record = self.record_lookup_func(child_content.record_uid, + context=context) is not None self.logger.debug(f" record exists in the vault: {existing_record}") else: self.logger.debug(f" record lookup function not defined, record existing: {existing_record}") @@ -706,12 +710,12 @@ def _process_auto_add_level(self, # If the rules have set the admin_uid then connect the user to the resource. if (child_content.admin_uid is not None and child_content.record_type != PAM_USER - and record_lookup_func is not None): + and self.record_lookup_func is not None): self.logger.debug("the admin UID has been set for this resource") - admin_record = record_lookup_func(record_uid=child_content.admin_uid, - context=context) # type: NormalizedRecord + admin_record = self.record_lookup_func(child_content.admin_uid, + context=context) # type: NormalizedRecord if admin_record is not None and admin_record.record_type == PAM_USER: self.logger.debug("was able to find the admin record, connect to resource") admin_uid = child_content.admin_uid @@ -745,7 +749,6 @@ def _process_auto_add_level(self, current_vertex=vertex, bulk_add_records=bulk_add_records, bulk_convert_records=bulk_convert_records, - record_lookup_func=record_lookup_func, record_prepare_func=record_prepare_func, directory_info_func=directory_info_func, record_cache=record_cache, @@ -769,7 +772,6 @@ def _process_level(self, current_vertex: DAGVertex, bulk_add_records: List[BulkRecordAdd], bulk_convert_records: List[BulkRecordConvert], - record_lookup_func: Callable, prompt_func: Callable, prompt_admin_func: Callable, record_prepare_func: Callable, @@ -837,7 +839,7 @@ def _process_level(self, # not created a record for yet, however it might have been assigned a record UID from a prior prompt. existing_record = child_content.record_exists - if record_lookup_func is not None: + if self.record_lookup_func is not None: check_the_vault = True for item in bulk_add_records: if item.record_uid == child_content.record_uid: @@ -845,8 +847,8 @@ def _process_level(self, check_the_vault = False break if check_the_vault: - existing_record = record_lookup_func(record_uid=child_content.record_uid, - context=context) is not None + existing_record = self.record_lookup_func(child_content.record_uid, + context=context) is not None self.logger.debug(f" record exists in the vault: {existing_record}") else: self.logger.debug(f" record lookup function not defined, record existing: {existing_record}") @@ -965,12 +967,12 @@ def _process_level(self, self.logger.debug("checking if can add admin") # If the rule engine sets the admin UID - if child_content.admin_uid is not None and record_lookup_func is not None: + if child_content.admin_uid is not None and self.record_lookup_func is not None: self.logger.debug(f"the resource rule set the admin uid to {add_content.admin_uid}") - admin_record = record_lookup_func(record_uid=add_content.admin_uid, - context=context) # type: NormalizedRecord + admin_record = self.record_lookup_func(add_content.admin_uid, + context=context) # type: NormalizedRecord if admin_record is not None and admin_record.record_type == PAM_USER: self.logger.debug("was able to find the admin record, connect to resource") @@ -1015,7 +1017,6 @@ def _process_level(self, domain=source, admin_acl=acl, directory_info_func=directory_info_func, - record_lookup_func=record_lookup_func, context=context, user=add_content.access_user.user, dn=add_content.access_user.dn @@ -1040,7 +1041,6 @@ def _process_level(self, resource_content=add_content, bulk_add_records=bulk_add_records, bulk_convert_records=bulk_convert_records, - record_lookup_func=record_lookup_func, directory_info_func=directory_info_func, prompt_admin_func=prompt_admin_func, record_prepare_func=record_prepare_func, @@ -1069,7 +1069,6 @@ def _process_level(self, current_vertex=vertex, bulk_add_records=bulk_add_records, bulk_convert_records=bulk_convert_records, - record_lookup_func=record_lookup_func, prompt_func=prompt_func, prompt_admin_func=prompt_admin_func, record_prepare_func=record_prepare_func, @@ -1088,7 +1087,6 @@ def _process_admin_user(self, resource_content: DiscoveryObject, bulk_add_records: List[BulkRecordAdd], bulk_convert_records: List[BulkRecordConvert], - record_lookup_func: Callable, directory_info_func: Callable, prompt_admin_func: Callable, record_prepare_func: Callable, @@ -1199,7 +1197,6 @@ def _process_admin_user(self, domain=ad_domain, admin_acl=admin_acl, directory_info_func=directory_info_func, - record_lookup_func=record_lookup_func, context=context, user=admin_content.item.user, dn=admin_content.item.dn @@ -1366,7 +1363,6 @@ def run(self, prompt_func: Callable, record_prepare_func: Callable, smart_add: bool = False, - record_lookup_func: Optional[Callable] = None, record_create_func: Optional[Callable] = None, record_convert_func: Optional[Callable] = None, prompt_confirm_add_func: Optional[Callable] = None, @@ -1383,7 +1379,6 @@ def run(self, :param record_cache: A dictionary of record types to keys to record UID. :param prompt_func: Function to call when the user needs to make a decision about an object. :param smart_add: If we have resource cred, add the resource and the users. DEPRECATED - :param record_lookup_func: Function to look up a record by UID. :param record_prepare_func: Function to call to prepare a record to be created. :param record_create_func: Function to call to save the prepared records. :param record_convert_func: Function to convert record to use this gateway. @@ -1459,7 +1454,6 @@ def run(self, current_vertex=configuration, bulk_add_records=bulk_add_records, bulk_convert_records=bulk_convert_records, - record_lookup_func=record_lookup_func, record_prepare_func=record_prepare_func, directory_info_func=directory_info_func, record_cache=record_cache, @@ -1482,7 +1476,6 @@ def run(self, current_vertex=configuration, bulk_add_records=bulk_add_records, bulk_convert_records=bulk_convert_records, - record_lookup_func=record_lookup_func, prompt_func=prompt_func, prompt_admin_func=prompt_admin_func, record_prepare_func=record_prepare_func, diff --git a/keepercommander/discovery_common/rm_types.py b/keepercommander/discovery_common/rm_types.py index 1aa513d88..6829da93a 100644 --- a/keepercommander/discovery_common/rm_types.py +++ b/keepercommander/discovery_common/rm_types.py @@ -84,6 +84,105 @@ class RmGroup(BaseModel): users: List[RmUser] = [] +class RmPermission(BaseModel): + """ + A permission granted by a role, reduced to its action strings. + + :param id: Provider identifier for the permission source (policy ARN, role-definition id, etc.). + :param name: Human-readable name of the permission source. + :param source: Where the permission comes from, e.g. "managed-policy", "inline-policy", + "role-definition". + :param actions: The granted action strings (e.g. "s3:GetObject", "Microsoft.Compute/*"). + """ + id: Optional[str] = None + name: Optional[str] = None + source: Optional[str] = None + actions: List[str] = [] + + +class RmMappedUser(BaseModel): + """ + A user in the standing-permissions map, with the roles it holds. + + :param id: Provider identifier for the user. + :param name: Human-readable user name. + :param role_ids: Ids of the roles this user holds (references RmMappedRole.id). + """ + id: str + name: Optional[str] = None + role_ids: List[str] = [] + + +class RmMappedGroup(BaseModel): + """ + A group in the standing-permissions map, with its members and the roles it holds. + + :param id: Provider identifier for the group. + :param name: Human-readable group name. + :param user_ids: Ids of the users that are members of this group (references RmMappedUser.id). + :param role_ids: Ids of the roles this group holds (references RmMappedRole.id). + Empty for providers with no group-role concept (e.g. AWS IAM). + """ + id: str + name: Optional[str] = None + user_ids: List[str] = [] + role_ids: List[str] = [] + + +class RmMappedServiceAccount(BaseModel): + """ + A service account in the standing-permissions map, with the roles it holds. + + A service account is a non-human principal: an Azure service principal or managed identity, + a GCP service account, etc. + + :param id: Provider identifier for the service account. + :param name: Human-readable service account name. + :param role_ids: Ids of the roles this service account holds (references RmMappedRole.id). + """ + id: str + name: Optional[str] = None + role_ids: List[str] = [] + + +class RmMappedRole(BaseModel): + """ + A role in the standing-permissions map, with the permissions it grants. + + :param id: Provider identifier for the role. + :param name: Human-readable role name. + :param permissions: The permissions granted by this role. + """ + id: str + name: Optional[str] = None + permissions: List[RmPermission] = [] + + +class RmStandingPermissions(BaseModel): + """ + The standing-permissions map for one identity provider. + + Relationships are normalized: roles are listed once with their permissions, while users and + groups reference roles by id. Group membership is carried as user ids on each group. + + :param provider: The identity provider the map was built for, e.g. "aws", "azure". + :param configuration_uid: The PAM configuration record UID the map was built from. + :param supported: False when the provider does not support standing-permission mapping + (e.g. GCP, Okta); the principal/role lists are then empty. + :param users: Users and the roles they hold. + :param groups: Groups, their members, and the roles they hold. + :param service_accounts: Service accounts (non-human principals) and the roles they hold. + :param roles: Roles and the permissions they grant. + """ + provider: str + configuration_uid: Optional[str] = None + supported: bool = True + users: List[RmMappedUser] = [] + groups: List[RmMappedGroup] = [] + service_accounts: List[RmMappedServiceAccount] = [] + roles: List[RmMappedRole] = [] + + class RmMetaBase(BaseModel): pass @@ -159,6 +258,10 @@ class RmOktaUserAddMeta(RmMetaBase): password_reset_required: Optional[bool] = False password_reset_required_with_mfa: Optional[bool] = False groups: List[str] = [] + # Okta app-instance ids ("0oa...") to assign the user to directly at creation, in addition to + # group-based access. Used for launch-URL flows (the pamCloudResource "Account Identifier") so + # an ephemeral user can reach the specific app the session launches into. + app_ids: List[str] = [] class RmOktaGroupAddMeta(RmMetaBase): @@ -503,6 +606,10 @@ class RmMachineUserAddMeta(RmMetaBase): use_private_key_type: Optional[str] = "ecdsa_sha2_nistp521" private_key: Optional[str] = None private_key_passphrase: Optional[str] = None + private_key_format: Optional[str] = None + public_key_format: Optional[str] = None + private_key_encoding: Optional[str] = None + public_key_encoding: Optional[str] = None authorized_keys: List[str] = [] # SSH certificate diff --git a/keepercommander/discovery_common/types.py b/keepercommander/discovery_common/types.py index 83512ab9b..04ecbae81 100644 --- a/keepercommander/discovery_common/types.py +++ b/keepercommander/discovery_common/types.py @@ -346,6 +346,25 @@ def get_schedule(self) -> Optional[dict]: return json.loads(self.schedule) +class UserAclServiceNamesItem(BaseModel): + name: str + + # If this was added via Discovery, this will be True + via_discovery: bool = False + + +class ServiceEnum(BaseEnum): + service = "service" + task = "task" + iis_pool = "iis_pool" + dcom = "dcom" + + +class UserAclServiceNames(BaseModel): + type: ServiceEnum + items: List[UserAclServiceNamesItem] = [] + + class UserAcl(BaseModel): # Vault team may add attributes without telling others :( model_config = ConfigDict(extra='allow') @@ -368,6 +387,10 @@ class UserAcl(BaseModel): # Do we need to rotate service passwords on this machine when this password is rotated? controls_services: bool = False + # For the relationship between the user and machine, these are services the user controls. + # The value is encrypted. + service_names: Optional[str] = "" + rotation_settings: Optional[UserAclRotationSettings] = None @staticmethod @@ -379,6 +402,58 @@ def default(): rotation_settings=UserAclRotationSettings() ) + def md5(self, record_key_bytes: bytes) -> str: + """ + Make an MD5 signature of this ACL. + + Dictionary keys and list items are sorted so the signature is repeatable + regardless of the order items appear in an array. The encrypted service_names + is replaced with its decrypted content; encryption is not deterministic, so + hashing the ciphertext would change the signature even when the content did not. + """ + + def _normalize(value: Any) -> Any: + if isinstance(value, dict): + return {k: _normalize(v) for k, v in value.items()} + if isinstance(value, list): + return sorted( + (_normalize(item) for item in value), + key=lambda item: json.dumps(item, sort_keys=True) + ) + return value + + data = self.model_dump(mode="json") + data["service_names"] = [ + service_names.model_dump(mode="json") + for service_names in self.get_service_names(record_key_bytes) + ] + data = _normalize(data) + return hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest() + + def set_service_names(self, + service_names: Union[List[UserAclServiceNames], list, str, bytes], + record_key_bytes: bytes): + if isinstance(service_names, list): + service_names = json.dumps([ + item.model_dump(mode="json") if isinstance(item, UserAclServiceNames) else item + for item in service_names + ]) + if isinstance(service_names, str): + service_names = service_names.encode() + + if not isinstance(service_names, bytes): + raise ValueError("The service_names is not a list, string or is bytes.") + + self.service_names = base64.b64encode(CryptoUtils.encrypt_aes(service_names, record_key_bytes)).decode() + + def get_service_names(self, record_key_bytes: bytes) -> List[UserAclServiceNames]: + if self.service_names is None or self.service_names == "": + return [] + enc_bytes = base64.b64decode(self.service_names.encode()) + service_names_bytes = CryptoUtils.decrypt_aes(enc_bytes, record_key_bytes) + list_of_service_names = json.loads(service_names_bytes) + return [UserAclServiceNames.model_validate(item) for item in list_of_service_names] + # ------------- @@ -475,6 +550,7 @@ class Facts(BaseModel): services: List[FactsNameUser] = [] tasks: List[FactsNameUser] = [] iis_pools: List[FactsNameUser] = [] + dcoms: List[FactsNameUser] = [] @property def has_services(self): @@ -488,9 +564,13 @@ def has_tasks(self): def has_iis_pools(self): return self.iis_pools is not None and len(self.iis_pools) > 0 + @property + def has_dcoms(self): + return self.dcoms is not None and len(self.dcoms) > 0 + @property def has_service_items(self): - return self.has_services or self.has_tasks or self.has_iis_pools + return self.has_services or self.has_tasks or self.has_iis_pools or self.has_dcoms class DiscoveryMachine(DiscoveryItem): @@ -657,6 +737,7 @@ class NormalizedRecord(BaseModel): """ record_uid: str record_type: str + record_key_bytes: Optional[bytes] = None title: str fields: List[RecordField] = [] note: Optional[str] = None diff --git a/keepercommander/discovery_common/user_service.py b/keepercommander/discovery_common/user_service.py index 04aab341c..328903561 100644 --- a/keepercommander/discovery_common/user_service.py +++ b/keepercommander/discovery_common/user_service.py @@ -4,13 +4,14 @@ from .constants import PAM_MACHINE, PAM_USER, PAM_DIRECTORY, DOMAIN_USER_CONFIGS from .utils import get_connection, make_agent, split_user_and_domain, value_to_boolean -from .types import DiscoveryObject, ServiceAcl, NormalizedRecord, UserAcl +from .types import (DiscoveryObject, ServiceAcl, NormalizedRecord, UserAcl, UserAclServiceNames, + UserAclServiceNamesItem, ServiceEnum) from .infrastructure import Infrastructure from .record_link import RecordLink from ..keeper_dag import DAG, EdgeType from ..keeper_dag.types import PamGraphId import importlib -from typing import Any, Optional, List, Callable, Dict, TYPE_CHECKING +from typing import Any, Optional, List, Callable, Dict, Tuple, TYPE_CHECKING if TYPE_CHECKING: from ..keeper_dag.vertex import DAGVertex @@ -39,6 +40,7 @@ class UserService: def __init__(self, record_linking: RecordLink, record: Any, + record_lookup_func: Callable, logger: Optional[Any] = None, history_level: int = 0, debug_level: int = 0, @@ -47,15 +49,23 @@ def __init__(self, save_batch_count: int = 200, agent: Optional[str] = None, use_per_graph_endpoints: bool = False, + context: Optional[Dict] = None, **kwargs): # Keep these for other graphs self._params = kwargs.get("params") self._ksm = kwargs.get("ksm") self.record_linking = record_linking + self.record_lookup_func = record_lookup_func self.service_graph = None self.did_migration = False + if context is None: + context = {} + if self._params is not None: + context["params"] = self._params + self.context = context + self.conn = get_connection(**kwargs) # This will either be a KSM Record, or Commander KeeperRecord @@ -91,6 +101,8 @@ def __init__(self, except (Exception,): pass + self._infra = None + self._migrate() def debug(self, msg, level: int = 0, secret: bool = False): @@ -101,6 +113,21 @@ def debug(self, msg, level: int = 0, secret: bool = False): else: self.logger.debug(msg) + @property + def record_key_bytes(self) -> bytes: + # The KSM Record exposes record_key_bytes; the Commander KeeperRecord exposes record_key. + if hasattr(self.record, "record_key_bytes"): + return self.record.record_key_bytes + return self.record.record_key + + @property + def infra(self): + + if self._infra is None: + self._infra = Infrastructure(record=self.record, logger=self.logger, ksm=self._ksm, params=self._params) + self._infra.load(sync_point=0) + return self._infra + def _migrate(self): """ @@ -122,25 +149,93 @@ def _migrate(self): save_batch_count=self.save_batch_count, agent=self.agent) + # Cannot add service names via migration. Not enough data in graph to perform it. + + # First migration - ServiceACL + # If still using the service_graph, migrate to the PAM graph. + # Add a generic label per service type being used. self.service_graph.load(sync_point=0) - if self.service_graph.has_graph: - for resource_vertex in self.service_graph.get_root.has_vertices(): - for user_vertex in resource_vertex.has_vertices(): - acl_edge = user_vertex.get_edge(resource_vertex, edge_type=EdgeType.ACL) + updated_service_graph = False + if self.service_graph.has_graph and len(self.service_graph.get_root.has_vertices()) > 0: + self.logger.debug("migrate from user service graph") + for s_resource_vertex in self.service_graph.get_root.has_vertices(): + for s_user_vertex in s_resource_vertex.has_vertices(): + acl_edge = s_user_vertex.get_edge(s_resource_vertex, edge_type=EdgeType.ACL) if acl_edge is not None: + service_acl = acl_edge.content_as_object(ServiceAcl) # type: ServiceAcl if service_acl.is_used: - user_acl = self.record_linking.get_acl(user_vertex.uid, resource_vertex.uid) + self.logger.debug(f" * {s_resource_vertex.uid} <- {s_user_vertex.uid}") + + record = self.record_lookup_func(s_user_vertex.uid, context=self.context) + user_acl = self.record_linking.get_acl(s_user_vertex.uid, s_resource_vertex.uid) if user_acl is None: user_acl = UserAcl.default() - user_acl.controls_services = True - self.record_linking.belongs_to(user_vertex.uid, resource_vertex.uid, acl=user_acl) - user_vertex.delete() - resource_vertex.delete() + + if record is not None: + + # Set via_discovery=True so that discovery will remove these and replace with + # real services, tasks, and IIS pools. + + if service_acl.is_service: + self.add_service_name(acl=user_acl, + service_type=ServiceEnum.service, + service_name="Unknown Service(s)", + record_key_bytes=record.record_key_bytes, + via_discovery=True) + self.logger.debug(f" - user controls service") + + if service_acl.is_task: + self.add_service_name(acl=user_acl, + service_type=ServiceEnum.task, + service_name="Unknown Task(s)", + record_key_bytes=record.record_key_bytes, + via_discovery=True) + self.logger.debug(f" - user controls task") + + if service_acl.is_iis_pool: + self.add_service_name(acl=user_acl, + service_type=ServiceEnum.iis_pool, + service_name="Unknown IIS Pool(s)", + record_key_bytes=record.record_key_bytes, + via_discovery=True) + self.logger.debug(f" - user controls iis pool") + else: + self.logger.debug(f" !! could not find user record UID {s_user_vertex.uid}") + user_acl.controls_services = True + + # set_acl also links the resource to the root; without that link the + # migrated vertices would not survive a save/load of the graph. + self.set_acl(resource_uid=s_resource_vertex.uid, user_uid=s_user_vertex.uid, acl=user_acl) + s_user_vertex.delete() + s_resource_vertex.delete() + updated_service_graph = True self.did_migration = True + # Second Migration - UserACL + # If migrated to the PAM graph, and there are no service_names, add a generic label for all services. + for resource_vertex in self.record_linking.dag.get_root.has_vertices(): + for user_vertex in resource_vertex.has_vertices(): + user_acl = self.record_linking.get_acl(record_uid=user_vertex.uid, + parent_record_uid=resource_vertex.uid) + if user_acl: + # If the user controls services and the names are blank + if user_acl.controls_services and (user_acl.service_names is None + or user_acl.service_names == ""): + record = self.record_lookup_func(user_vertex.uid, context=self.context) + if record: + self.logger.debug(f" * {resource_vertex.uid} <- {user_vertex.uid}; add placeholder") + self.did_migration = True + self.add_service_name(acl=user_acl, + service_type=ServiceEnum.service, + service_name="Unknown Service, Task, or IIS Pools", + record_key_bytes=record.record_key_bytes, + via_discovery=True) + self.record_linking.belongs_to(user_vertex.uid, resource_vertex.uid, acl=user_acl) + if self.did_migration: - self.service_graph.save() + if updated_service_graph: + self.service_graph.save() self.record_linking.save() self.debug(" items were migrated") else: @@ -156,6 +251,7 @@ def close(self): self.conn = None self._params = None self._ksm = None + self._infra = None def __enter__(self): """Context manager entry.""" @@ -331,63 +427,15 @@ def to_dot(self, graph_format: str = "svg", graph_type: str = "dot"): return dot - def _init_cleanup_user_mapping(self): - - """ - Create of mapping of existing user services to see what was updated. - - This is the basically graph in dictionary format with the update flag set to False. - """ - - self.cleanup_mapping = {} - for resource_vertex in self.record_linking.dag.get_root.has_vertices(): - if resource_vertex.uid not in self.cleanup_mapping: - self.cleanup_mapping[resource_vertex.uid] = {} - for user_vertex in resource_vertex.has_vertices(): - acl = self.record_linking.get_acl(user_vertex.uid, resource_vertex.uid) - if acl is not None and acl.controls_services: - self.cleanup_mapping[resource_vertex.uid][user_vertex.uid] = False - - def _user_is_used(self, machine_record_uid: str, user_record_uid: str): - - """ - Flag the user exists for a machine. - """ - - if machine_record_uid in self.cleanup_mapping and user_record_uid in self.cleanup_mapping[machine_record_uid]: - self.cleanup_mapping[machine_record_uid][user_record_uid] = True - - def _cleanup_users(self): - - """ - Disconnect all users from machines that are not used. - """ - - self.debug("cleaning up unused user service relationships") - - did_something = False - for machine_record_uid in self.cleanup_mapping: - for user_record_uid in self.cleanup_mapping[machine_record_uid]: - if not self.cleanup_mapping[machine_record_uid][user_record_uid]: - acl = self.record_linking.get_acl(user_record_uid, machine_record_uid) - if acl is not None and acl.controls_services: - self.debug(f" * set controls_services for user {user_record_uid} " - f"from machine {machine_record_uid} to false") - did_something = True - acl.controls_services = False - self.record_linking.belongs_to(user_record_uid, machine_record_uid, acl=acl) - if not did_something: - self.debug(f" nothing to cleanup") - - @staticmethod - def _get_local_users_from_record(record_lookup_func: Callable, - rl_machine_vertex: DAGVertex) -> Dict[str, str]: + def _get_local_users_from_record(self, rl_machine_vertex: DAGVertex) -> Dict[str, str]: # Get the local users user_records: Dict[str, str] = {} for rl_user_vertex in rl_machine_vertex.has_vertices(): - record = record_lookup_func(rl_user_vertex.uid, allow_sm=False) # type: NormalizedRecord + record = self.record_lookup_func(rl_user_vertex.uid, + context=self.context, + allow_sm=False) # type: NormalizedRecord if record and record.record_type == PAM_USER: user = record.get_user() if user is not None: @@ -404,16 +452,14 @@ def _get_local_users_from_record(record_lookup_func: Callable, return user_records - @staticmethod - def _get_local_users_from_infra(record_lookup_func: Callable, - infra_machine_vertex: DAGVertex) -> Dict[str, str]: + def _get_local_users_from_infra(self, infra_machine_vertex: DAGVertex) -> Dict[str, str]: user_records: Dict[str, str] = {} for infra_user_vertex in infra_machine_vertex.has_vertices(): user_content = DiscoveryObject.get_discovery_object(infra_user_vertex) if user_content.record_type != PAM_USER or user_content.record_uid is None: continue - if record_lookup_func(user_content.record_uid, allow_sm=False): + if self.record_lookup_func(user_content.record_uid, context=self.context, allow_sm=False): user, domain = split_user_and_domain(user_content.item.user.lower()) if domain is not None: user += "@" + domain @@ -423,15 +469,15 @@ def _get_local_users_from_infra(record_lookup_func: Callable, def _get_directory_users_from_conf_record(self, domain_name: str, - record_lookup_func: Callable, netbios: Optional[str] = None) -> Dict[str, str]: user_records: Dict[str, str] = {} # check if a PAM configuration that support having users (Azure, Domain Controller) # We need to get the normalized record of the configuration record. - configuration_record = record_lookup_func( - self.conn.get_record_uid(self.record), allow_sm=False) # type: NormalizedRecord + configuration_record = self.record_lookup_func( self.conn.get_record_uid(self.record), + context=self.context, + allow_sm=False) # type: NormalizedRecord if configuration_record.record_type in DOMAIN_USER_CONFIGS: # The Domain Controller record will have the domain; Azure record will not. config_domain_name = configuration_record.get_value(label="pamdomainid") @@ -441,7 +487,9 @@ def _get_directory_users_from_conf_record(self, or (config_domain_name.lower() == domain_name or config_domain_name.lower() == netbios)): config_vertex = self.record_linking.dag.get_vertex(configuration_record.record_uid) for child_vertex in config_vertex.has_vertices(): - user_record = record_lookup_func(child_vertex.uid, allow_sm=False) # type: NormalizedRecord + user_record = self.record_lookup_func(child_vertex.uid, + context=self.context, + allow_sm=False) # type: NormalizedRecord if not user_record: # self.debug(f" * record uid {child_vertex.uid} not found") continue @@ -471,7 +519,6 @@ def _get_directory_users_from_conf_record(self, def _get_directory_users_from_conf_infra(self, infra: Infrastructure, domain_name: str, - record_lookup_func: Callable, netbios: Optional[str] = None) -> Dict[str, str]: user_records: Dict[str, str] = {} @@ -485,8 +532,9 @@ def _get_directory_users_from_conf_infra(self, self.debug(f" domain name {config_domain_name} MATCHED {domain_name}/{netbios}") for child_vertex in config_vertex.has_vertices(): child_context = DiscoveryObject.get_discovery_object(child_vertex) - if child_context.record_type == PAM_USER and record_lookup_func(child_context.record_uid, - allow_sm=False): + if child_context.record_type == PAM_USER and self.record_lookup_func(child_context.record_uid, + context=self.context, + allow_sm=False): user, domain = split_user_and_domain(child_context.item.user.lower()) if domain is None: domain = domain_name @@ -499,15 +547,16 @@ def _get_directory_users_from_conf_infra(self, return user_records def _get_directory_users_from_records(self, - domain_name: str, - record_lookup_func: Callable) -> Dict[str, str]: + domain_name: str) -> Dict[str, str]: user_records: Dict[str, str] = {} # From the record linking graph, check each record connected to the configuration to see if it is a # PAM directory record. for rl_resource_vertex in self.record_linking.dag.get_root.has_vertices(): - directory_record = record_lookup_func(rl_resource_vertex.uid, allow_sm=False) # type: NormalizedRecord + directory_record = self.record_lookup_func(rl_resource_vertex.uid, + context=self.context, + allow_sm=False) # type: NormalizedRecord if directory_record and directory_record.record_type == PAM_DIRECTORY: record_domain_name = directory_record.get_value(label="domainName") if record_domain_name is None: @@ -517,7 +566,9 @@ def _get_directory_users_from_records(self, if record_domain_name.lower() == domain_name: self.debug(f" record uid {rl_resource_vertex.uid} matches the domain name") for rl_user_vertex in rl_resource_vertex.has_vertices(): - user_record = record_lookup_func(rl_user_vertex.uid, allow_sm=False) # type: NormalizedRecord + user_record = self.record_lookup_func(rl_user_vertex.uid, + context=self.context, + allow_sm=False) # type: NormalizedRecord if user_record is None or user_record.record_type != PAM_USER: continue @@ -541,10 +592,9 @@ def _get_directory_users_from_records(self, return user_records - @staticmethod - def _get_directory_users_from_infra(infra_machine_vertex: DAGVertex, - domain_name: str, - record_lookup_func: Callable) -> Dict[str, str]: + def _get_directory_users_from_infra(self, + infra_machine_vertex: DAGVertex, + domain_name: str) -> Dict[str, str]: user_records: Dict[str, str] = {} @@ -561,7 +611,7 @@ def _get_directory_users_from_infra(infra_machine_vertex: DAGVertex, user_content = DiscoveryObject.get_discovery_object(user_vertex) if user_content.record_type != PAM_USER and user_content.record_uid is None: continue - if record_lookup_func(user_content.record_uid, allow_sm=False): + if self.record_lookup_func(user_content.record_uid, context=self.context, allow_sm=False): # Format the username to be user@domain user, domain = split_user_and_domain(user_content.item.user.lower()) @@ -575,7 +625,6 @@ def _get_users(self, infra: Infrastructure, infra_machine_content: DiscoveryObject, infra_machine_vertex: DAGVertex, - record_lookup_func: Callable, netbios: Optional[str] = None, domain_name: Optional[str] = None) -> Dict[str, str]: @@ -607,10 +656,15 @@ def _get_users(self, # Once we get directory users for a domain name, they will not change. # Cache them so we don't have to get them again. if self.directory_user_cache is not None: - directory_user_records = self.directory_user_cache.get(domain_name) - self.debug(f" using directory user cache for {domain_name}, " - f"{len(directory_user_records)} users") - using_directory_user_cache = True + directory_user_records = self.directory_user_cache.get(domain_name, {}) + if directory_user_records is not None and len(directory_user_records) > 0: + self.debug(f" using directory user cache for {domain_name}, " + f"{len(directory_user_records)} users") + using_directory_user_cache = True + # If directory_user_records is None, make sure it's an empty dictionary. + # We might try to merge dictionaries; we don't want it None. + else: + directory_user_records = {} ########################### @@ -623,8 +677,7 @@ def _get_users(self, # Get the local users from records self.debug(" getting local users from records", level=1) - user_records = self._get_local_users_from_record(rl_machine_vertex=record_link_vertex, - record_lookup_func=record_lookup_func) + user_records = self._get_local_users_from_record(rl_machine_vertex=record_link_vertex) self.debug(f" * found {len(user_records)} local users from records", level=1) local_user_records = {**local_user_records, **user_records} @@ -632,16 +685,14 @@ def _get_users(self, self.debug(" getting directory users from the configuration record", level=1) user_records = self._get_directory_users_from_conf_record(domain_name=domain_name, - netbios=netbios, - record_lookup_func=record_lookup_func) + netbios=netbios) self.debug(f" * found {len(user_records)} directory users records from " "the configuration record", level=1) directory_user_records = {**directory_user_records, **user_records} self.debug(" getting directory users from directory records", level=1) - user_records = self._get_directory_users_from_records(domain_name=domain_name, - record_lookup_func=record_lookup_func) + user_records = self._get_directory_users_from_records(domain_name=domain_name) self.debug(f" * found {len(user_records)} directory users from records for {domain_name}", level=1) @@ -653,8 +704,7 @@ def _get_users(self, self.debug(f" getting users from infrastructure", level=1) self.debug(" getting local users from infrastructure", level=1) - user_records = self._get_local_users_from_infra(infra_machine_vertex=infra_machine_vertex, - record_lookup_func=record_lookup_func) + user_records = self._get_local_users_from_infra(infra_machine_vertex=infra_machine_vertex) self.debug(f" * found {len(user_records)} local users from graph", level=1) local_user_records = {**user_records, **local_user_records} @@ -662,8 +712,7 @@ def _get_users(self, self.debug(" getting directory users from configuration infrastructure", level=1) user_records = self._get_directory_users_from_conf_infra(infra=infra, - domain_name=domain_name, - record_lookup_func=record_lookup_func) + domain_name=domain_name) self.debug(f" * found {len(user_records)} directory users from configuration for {domain_name}", level=1) directory_user_records = {**user_records, **directory_user_records} @@ -672,9 +721,9 @@ def _get_users(self, self.debug(" getting directory users from directory infrastructure", level=1) user_records = self._get_directory_users_from_infra(infra_machine_vertex=infra_machine_vertex, - domain_name=domain_name, - record_lookup_func=record_lookup_func) - self.debug(f" * found {len(user_records)} directory users from graph for {domain_name}", level=1) + domain_name=domain_name) + self.debug(f" * found {len(user_records)} directory users from graph for {domain_name}", + level=1) directory_user_records = {**user_records, **directory_user_records} # If we were not using the directory cache, cache them. @@ -689,14 +738,91 @@ def _get_users(self, return all_record - def _connect_users_to_services(self, - infra: Infrastructure, - infra_machine_content: DiscoveryObject, - infra_machine_vertex: DAGVertex, - record_lookup_func: Callable, - strict: bool = False, - domain_name: Optional[str] = None, - netbios: Optional[str] = None): + @staticmethod + def clear_discovery_service_names(acl: UserAcl, record_key_bytes: bytes): + + """ + Remove all the service names added by discovery from the list. + """ + + new_service_names = [] + for service_names in acl.get_service_names(record_key_bytes): + service_names.items = [item for item in service_names.items if not item.via_discovery] + if service_names.items: + new_service_names.append(service_names) + acl.set_service_names(new_service_names, record_key_bytes) + + @staticmethod + def add_service_name(acl: UserAcl, + service_type: ServiceEnum, + service_name: str, + record_key_bytes: bytes, + via_discovery: bool = False): + + all_service_names = acl.get_service_names(record_key_bytes) + + service_names = None + for name in all_service_names: + if name.type == service_type: + service_names = name + break + + if service_names is None: + service_names = UserAclServiceNames(type=service_type) + all_service_names.append(service_names) + + for item in service_names.items: + if item.name.lower() == service_name.lower(): + return + + service_names.items.append( + UserAclServiceNamesItem( + name=service_name, + via_discovery=via_discovery) + ) + + acl.set_service_names(all_service_names, record_key_bytes) + + if len(all_service_names) > 0: + acl.controls_services = True + + @staticmethod + def remove_service_name(acl: UserAcl, + record_key_bytes: bytes, + service_type: Optional[ServiceEnum] = None, + service_name: Optional[str] = None): + + # Without a service type, clear all the service names for all types. + if service_type is None: + acl.service_names = "" + return + + all_service_names = acl.get_service_names(record_key_bytes) + + new_service_names = [] + for service_names in all_service_names: + if service_names.type == service_type: + # Without a service name, remove all the items for this service type. + if service_name is None: + continue + service_names.items = [item for item in service_names.items + if item.name.lower() != service_name.lower()] + if not service_names.items: + continue + new_service_names.append(service_names) + + if len(new_service_names) == 0: + acl.controls_services = False + + acl.set_service_names(new_service_names, record_key_bytes) + + def _connect_users_to_machine_services(self, + infra: Infrastructure, + infra_machine_content: DiscoveryObject, + infra_machine_vertex: DAGVertex, + strict: bool = False, + domain_name: Optional[str] = None, + netbios: Optional[str] = None): if domain_name is None: for directory in infra_machine_content.item.facts.directories: @@ -715,19 +841,41 @@ def _connect_users_to_services(self, if hasattr(config_object.item, "info"): netbios = config_object.item.info.get("netbios") + # We don't want to keep update the ACL in the graph, it will make a lot of edges. + # Update the cache, then update the graph at the end. + acl_cache: Dict[str, Tuple] = {} + acl_md5_cache: Dict[str, str] = {} + + # These are the user available on a machine. + users = self._get_users(infra=infra, + infra_machine_content=infra_machine_content, + infra_machine_vertex=infra_machine_vertex, + netbios=netbios, + domain_name=domain_name) + + if self.log_finer_level >= 2 and self.insecure_debug: + for k, v in users.items(): + self.debug(f" > {k} = {v}") + # Add mapping from user to machine, that control services. - for service_type in ["service", "task", "iis_pool"]: + for service_type in [ServiceEnum.service, ServiceEnum.task, ServiceEnum.iis_pool]: self.debug("-" * 40) - self.debug(f"processing {service_type}s for {infra_machine_content.name} " + self.debug(f"processing {service_type.value}s for {infra_machine_content.name} " f"({infra_machine_vertex.uid})") - # We don't care about the name of the service, we just need a list users. + # Get the pair of name of the service and the user that controls it. + # This is from discovery. + service_pairs = getattr(infra_machine_content.item.facts, f"{service_type.value}s") + if len(service_pairs) == 0: + self.debug(" no users control this type of service, skipping") + continue + + for service_pair in service_pairs: + self.debug(f" * {service_type.value}: {service_pair.name} ({service_pair.user})", secret=True) - service_users = [] - for service_user in getattr(infra_machine_content.item.facts, f"{service_type}s"): - self.debug(f" * {service_type}: {service_user.name} ({service_user.user})", secret=True) - user = service_user.user.lower() + user = service_pair.user.lower() + service_users = [] if not strict: user, domain = split_user_and_domain(user) if user is not None: @@ -742,48 +890,56 @@ def _connect_users_to_services(self, else: service_users.append(user) + # de-dup the list service_users = list(set(service_users)) if len(service_users) == 0: - self.debug(f" no users control {service_type}s, skipping.") + self.debug(f" no users control {service_type.value}s, skipping.") continue - users = self._get_users(infra=infra, - infra_machine_content=infra_machine_content, - infra_machine_vertex=infra_machine_vertex, - record_lookup_func=record_lookup_func, - netbios=netbios, - domain_name=domain_name) - - if self.log_finer_level >= 2 and self.insecure_debug: - for k, v in users.items(): - self.debug(f" > {k} = {v}") - - self.debug(f" users to check: {service_users}", secret=True) - for service_user in service_users: - self.debug(f" . {service_user}", secret=True) - if service_user in users: - record_uid = users[service_user] - self.debug(f" found user {service_user} for {service_type}", secret=True) - acl = self.get_acl(infra_machine_content.record_uid, record_uid) - if acl is None: - acl = UserAcl().default() - - # Flag the user was found; don't disconnect - self._user_is_used(machine_record_uid=infra_machine_content.record_uid, - user_record_uid=record_uid) - - # Only update if the attribute is currently False; reduce edges. - if not acl.controls_services: + self.debug(f" users to check: {service_users}", secret=True) + for service_user in service_users: + self.debug(f" . {service_user}", secret=True) + if service_user in users: + record_uid = users[service_user] + self.debug(f" found user {service_user} for {service_type.value}", secret=True) + + # Get the ACL edge from the cache. + # If not there load it from the graph. + # If not in the graph, create it, and cache it for the next time. + acl, record_key_bytes = acl_cache.get(record_uid, (None, None)) + if acl is None: + acl = self.get_acl(infra_machine_content.record_uid, record_uid) + if acl is None: + acl = UserAcl.default() + record = self.record_lookup_func(record_uid, context=self.context) # type: NormalizedRecord + record_key_bytes = record.record_key_bytes + acl_cache[record_uid] = (acl, record_key_bytes) + acl_md5_cache[record_uid] = acl.md5(record_key_bytes) + + # Remove all the items marked as discovery. + self.clear_discovery_service_names(acl, record_key_bytes) + acl.controls_services = True - self.set_acl(resource_uid=infra_machine_content.record_uid, - user_uid=record_uid, - acl=acl) + self.add_service_name(acl, + service_name=service_pair.name, + service_type=service_type, + record_key_bytes=record_key_bytes, + via_discovery=True) + + # Update the ACL for the machine. + for record_uid, acl_tuple in acl_cache.items(): + new_acl, record_key_bytes = acl_tuple + if new_acl.md5(record_key_bytes) != acl_md5_cache[record_uid]: + self.set_acl(resource_uid=infra_machine_content.record_uid, + user_uid=record_uid, + acl=new_acl) + acl_cache.clear() + acl_md5_cache.clear() def _get_resource_info(self, record_uid: str, infra: Infrastructure, - record_lookup_func: Callable, record_types: Optional[List[str]] = None) -> Optional[NormalizedRecord]: """ @@ -795,7 +951,7 @@ def _get_resource_info(self, """ # Check the record first; return a NormalizedRecord - record = record_lookup_func(record_uid, allow_sm=False) # type: NormalizedRecord + record = self.record_lookup_func(record_uid, context=self.context, allow_sm=False) # type: NormalizedRecord if record is not None: self.debug(f" resource is {record.title}") if record_types is not None and record.record_type not in record_types: @@ -830,7 +986,6 @@ def run_user(self): pass def run_full(self, - record_lookup_func: Callable, infra: Optional[Infrastructure] = None, domain_name: Optional[str] = None, netbios: Optional[str] = None, @@ -841,7 +996,6 @@ def run_full(self, This is driven by the record linking graph. :param infra: Instance of Infrastructure graph. - :param record_lookup_func: A function that will return a record by record id. Returns a normalize record. :param domain_name: Domain name if there is a directory (i.e. example.com) :param netbios: NetBIOS of the domain controller (i.e. EXMAPLE) """ @@ -856,10 +1010,6 @@ def run_full(self, created_infra = False try: - - # Make of map of the current user to machine relationship. - self._init_cleanup_user_mapping() - if not infra: infra = Infrastructure(record=self.record, logger=self.logger, ksm=self._ksm, params=self._params) infra.load(sync_point=0) @@ -877,7 +1027,6 @@ def run_full(self, # The results is a NormalizedRecord. machine_record = self._get_resource_info(record_uid=rl_resource_vertex.uid, infra=infra, - record_lookup_func=record_lookup_func, record_types=[PAM_MACHINE]) if machine_record is None: @@ -925,22 +1074,14 @@ def run_full(self, user_service_machine_vertex.belongs_to_root(EdgeType.LINK) self.debug("-" * 40) - self._connect_users_to_services( + self._connect_users_to_machine_services( infra=infra, infra_machine_content=infra_machine_content, infra_machine_vertex=infra_machine_vertex, - record_lookup_func=record_lookup_func, domain_name=domain_name, netbios=netbios) self.debug("-" * 40) - # Disconnect any users not used. - # TODO - Handle this better. - # If a machine is off, or we cannot connect, we might disconnect users. - # We can uses the infrastructure graph to see this. - # This needs more testing. - # self._cleanup_users() - self.save() except Exception as err: From ee2ef12d64203924c4ab148545440cd50854ab05 Mon Sep 17 00:00:00 2001 From: amangalampalli-ks Date: Wed, 12 Aug 2026 20:22:23 +0530 Subject: [PATCH 07/13] Require ownership before adopting service Docker setup folders and apps (#2281) (#2285) * Require ownership before adopting service Docker setup folders and apps * Use folder_add instead of mkdir to create duplicate name share folder --- keepercommander/commands/ksm.py | 2 +- .../integrations/integration_setup_base.py | 15 +- keepercommander/service/docker/setup_base.py | 183 +++++++--- .../service/test_docker_setup_ownership.py | 317 ++++++++++++++++++ 4 files changed, 464 insertions(+), 53 deletions(-) create mode 100644 unit-tests/service/test_docker_setup_ownership.py diff --git a/keepercommander/commands/ksm.py b/keepercommander/commands/ksm.py index 45d0aa61c..a80c5ad05 100644 --- a/keepercommander/commands/ksm.py +++ b/keepercommander/commands/ksm.py @@ -1757,7 +1757,7 @@ def add_new_v5_app(params, app_name, force_to_add=False, format_type='table'): logging.debug("Creating new KSM Application named '%s'" % app_name) found_app = KSMCommand.get_app_record(params, app_name) - if (found_app is not None) and (found_app is not force_to_add): + if found_app is not None and not force_to_add: if format_type == 'json': return json.dumps({"error": f'Application with the same name "{app_name}" already exists.'}) else: diff --git a/keepercommander/service/commands/integrations/integration_setup_base.py b/keepercommander/service/commands/integrations/integration_setup_base.py index ee7e783ad..aea4cb932 100644 --- a/keepercommander/service/commands/integrations/integration_setup_base.py +++ b/keepercommander/service/commands/integrations/integration_setup_base.py @@ -344,11 +344,10 @@ def _create_integration_record(self, params, record_name: str, return record_uid def _find_record_in_folder(self, params, folder_uid: str, record_name: str): - if folder_uid in params.subfolder_record_cache: - for rec_uid in params.subfolder_record_cache[folder_uid]: - rec = api.get_record(params, rec_uid) - if rec.title == record_name: - return rec_uid + for rec_uid in (params.subfolder_record_cache or {}).get(folder_uid, []): + rec = api.get_record(params, rec_uid) + if rec.title == record_name and self._is_owned_record(params, rec_uid): + return rec_uid return None def _create_login_record(self, params, folder_uid: str, record_name: str) -> str: @@ -394,9 +393,9 @@ def _patch_record_approvals(self, params, record_uid: str, approvals: ApprovalsC raise CommandError(self.get_command_name(), f'Failed to update record fields: {str(e)}') def _find_folder_uid_by_name(self, params, folder_name: str) -> Optional[str]: - # Prefer shared folders; integration setup always creates a shared folder. - for folder_uid, folder_data in params.shared_folder_cache.items(): - if folder_data.get('name') == folder_name: + """Return an owned shared folder UID with this name, or None.""" + for folder_uid, folder_data in (params.shared_folder_cache or {}).items(): + if folder_data.get('name') == folder_name and self._is_owned_shared_folder(params, folder_uid): return folder_uid return None diff --git a/keepercommander/service/docker/setup_base.py b/keepercommander/service/docker/setup_base.py index 49285225a..48fbb07bb 100644 --- a/keepercommander/service/docker/setup_base.py +++ b/keepercommander/service/docker/setup_base.py @@ -24,9 +24,8 @@ import tempfile from typing import Dict, Any -from ...commands.folder import FolderMakeCommand from ...commands.ksm import KSMCommand -from ... import api, vault, utils, attachment, record_management, loginv3 +from ... import api, vault, utils, crypto, attachment, record_management, loginv3 from ...display import bcolors from ...error import CommandError @@ -39,6 +38,51 @@ class DockerSetupBase: """Base class for Docker setup with reusable core logic""" + @staticmethod + def _is_owned_record(params, record_uid: str) -> bool: + """True when record_owner_cache marks the record as owned by this account.""" + if not record_uid: + return False + owner_cache = getattr(params, 'record_owner_cache', None) or {} + owner = owner_cache.get(record_uid) + return bool(owner and owner.owner) + + @staticmethod + def _is_owned_shared_folder(params, folder_uid: str) -> bool: + """True when the shared folder is owned by the current account.""" + if not folder_uid: + return False + shared_folder = (getattr(params, 'shared_folder_cache', None) or {}).get(folder_uid) + if not shared_folder: + return False + + owner_uid = shared_folder.get('owner_account_uid') + account_uid_bytes = getattr(params, 'account_uid_bytes', None) + if owner_uid and account_uid_bytes: + return owner_uid == utils.base64_url_encode(account_uid_bytes) + + owner_username = shared_folder.get('owner_username') + if owner_username and getattr(params, 'user', None): + return owner_username.lower() == params.user.lower() + return False + + @staticmethod + def _find_owned_ksm_app_by_title(params, app_name: str): + """Return an owned KSM app with this title, or None (ignores non-owned squats).""" + if not app_name: + return None + for rec_cache_val in (getattr(params, 'record_cache', None) or {}).values(): + rec = KSMCommand._app_record_from_cache_entry(rec_cache_val) + if not rec: + continue + try: + title = json.loads(rec['data_unencrypted'].decode('utf-8')).get('title') + except Exception: + continue + if title == app_name and DockerSetupBase._is_owned_record(params, rec.get('record_uid')): + return rec + return None + @staticmethod def resolve_commander_config_path(config_path: str = None, params=None) -> str: """Resolve config.json using the same rules as login/shell startup.""" @@ -163,57 +207,94 @@ def _setup_device(self, params, timeout: str) -> None: except Exception as e: raise CommandError('docker-setup', f'Device setup failed: {str(e)}') + @staticmethod + def _add_owned_shared_folder(params, folder_name: str) -> str: + """Create a root-level shared folder via folder_add (allows duplicate display names). + + Bypasses FolderMakeCommand/mkdir, which refuses to create when any visible + folder already has the same name — including a non-owned squat. + """ + name = (folder_name or '').strip() + if not name: + raise CommandError('docker-setup', 'Shared folder name is required') + if not getattr(params, 'data_key', None): + raise CommandError('docker-setup', 'Not logged in (missing data key)') + + folder_uid = api.generate_record_uid() + folder_key = utils.generate_aes_key() + request = { + 'command': 'folder_add', + 'folder_type': 'shared_folder', + 'folder_uid': folder_uid, + 'key': utils.base64_url_encode(crypto.encrypt_aes_v1(folder_key, params.data_key)), + 'name': utils.base64_url_encode(crypto.encrypt_aes_v1(name.encode('utf-8'), folder_key)), + 'data': utils.base64_url_encode( + crypto.encrypt_aes_v1(json.dumps({'name': name}).encode('utf-8'), folder_key) + ), + 'manage_users': True, + 'manage_records': True, + 'can_share': True, + 'can_edit': True, + } + api.communicate(params, request) + params.sync_data = True + return folder_uid + def _create_shared_folder(self, params, folder_name: str) -> str: - """Create shared folder or return existing one""" - # Check if folder exists - for folder_uid, folder in params.folder_cache.items(): - if folder.name == folder_name and folder_uid in params.shared_folder_cache: - DockerSetupPrinter.print_success("Using existing shared folder") + """Create a shared folder, or reuse one owned by this account with the same name.""" + saw_non_owned = False + for folder_uid, folder in (params.folder_cache or {}).items(): + if folder.name != folder_name or folder_uid not in (params.shared_folder_cache or {}): + continue + if self._is_owned_shared_folder(params, folder_uid): + DockerSetupPrinter.print_success(f"Using existing shared folder (UID: {folder_uid})") return folder_uid + saw_non_owned = True + + if saw_non_owned: + DockerSetupPrinter.print_warning( + f"Ignoring shared folder(s) named '{folder_name}' owned by another account; " + f"creating a new owned folder" + ) - # Create new folder try: - old_stdout = sys.stdout - sys.stdout = io.StringIO() - try: - folder_uid = FolderMakeCommand().execute( - params, - folder=folder_name, - shared_folder=True, - manage_users=True, - manage_records=True, - can_edit=True, - can_share=True - ) - finally: - sys.stdout = old_stdout + folder_uid = self._add_owned_shared_folder(params, folder_name) api.sync_down(params) DockerSetupPrinter.print_success(f"Shared folder created successfully (UID: {folder_uid})") return folder_uid + except CommandError: + raise except Exception as e: raise CommandError('docker-setup', f'Failed to create shared folder: {str(e)}') def _create_config_record(self, params, record_name: str, folder_uid: str) -> str: - """Create a config record or return existing one""" - # Check if record exists - if folder_uid in params.subfolder_record_cache: - for rec_uid in params.subfolder_record_cache[folder_uid]: - rec = api.get_record(params, rec_uid) - if rec.title == record_name: - DockerSetupPrinter.print_success("Using existing record") - return rec_uid - - # Create new record + """Create a config record, or reuse an owned one with the same title in the folder.""" + saw_non_owned = False + for rec_uid in (params.subfolder_record_cache or {}).get(folder_uid, []): + rec = api.get_record(params, rec_uid) + if rec.title != record_name: + continue + if self._is_owned_record(params, rec_uid): + DockerSetupPrinter.print_success("Using existing record") + return rec_uid + saw_non_owned = True + + if saw_non_owned: + DockerSetupPrinter.print_warning( + f"Ignoring record(s) titled '{record_name}' owned by another account; " + f"creating a new owned record" + ) + try: record = vault.KeeperRecord.create(params, 'login') record.record_uid = utils.generate_uid() record.record_key = utils.generate_aes_key() record.title = record_name record.type_name = 'login' - + record_management.add_record_to_folder(params, record, folder_uid) api.sync_down(params) - + DockerSetupPrinter.print_success(f"Record created successfully (UID: {record.record_uid})") return record.record_uid except Exception as e: @@ -336,32 +417,46 @@ def _clean_config_json(self, config_path: str) -> str: return config_path def _create_ksm_app(self, params, app_name: str) -> str: - """Create KSM app or return existing one""" - # Check if app exists - existing_app = KSMCommand.get_app_record(params, app_name) + """Create a KSM app, or reuse an owned app with the same title.""" + existing_app = self._find_owned_ksm_app_by_title(params, app_name) if existing_app: DockerSetupPrinter.print_success("Using existing app") return existing_app.get('record_uid') - # Create new app + existing_any = KSMCommand.get_app_record(params, app_name) + force_to_add = False + if existing_any: + existing_uid = existing_any.get('record_uid') + if self._is_owned_record(params, existing_uid): + DockerSetupPrinter.print_success("Using existing app") + return existing_uid + force_to_add = True + DockerSetupPrinter.print_warning( + f"Ignoring Secrets Manager app '{app_name}' owned by another account; " + f"creating a new owned app" + ) + try: - # Suppress KSM command output old_stdout = sys.stdout sys.stdout = io.StringIO() try: - KSMCommand.add_new_v5_app(params, app_name, force_to_add=False, format_type='table') + KSMCommand.add_new_v5_app( + params, app_name, force_to_add=force_to_add, format_type='table' + ) finally: sys.stdout = old_stdout - + api.sync_down(params) - - app_rec = KSMCommand.get_app_record(params, app_name) + + app_rec = self._find_owned_ksm_app_by_title(params, app_name) if not app_rec: raise CommandError('docker-setup', 'Failed to retrieve created app') - + app_uid = app_rec.get('record_uid') DockerSetupPrinter.print_success(f"App created successfully (UID: {app_uid})") return app_uid + except CommandError: + raise except Exception as e: raise CommandError('docker-setup', f'Failed to create KSM app: {str(e)}') diff --git a/unit-tests/service/test_docker_setup_ownership.py b/unit-tests/service/test_docker_setup_ownership.py new file mode 100644 index 000000000..2dbf4cb02 --- /dev/null +++ b/unit-tests/service/test_docker_setup_ownership.py @@ -0,0 +1,317 @@ +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' Date: Wed, 12 Aug 2026 19:21:58 -0700 Subject: [PATCH 08/13] epm scim: record user/group Id --- keepercommander/commands/pedm/pedm_admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keepercommander/commands/pedm/pedm_admin.py b/keepercommander/commands/pedm/pedm_admin.py index 4e7f75c67..278d55fe0 100644 --- a/keepercommander/commands/pedm/pedm_admin.py +++ b/keepercommander/commands/pedm/pedm_admin.py @@ -446,6 +446,7 @@ def build_user(user: ScimUser) -> Optional[Tuple[admin_types.CollectionData, boo return None key = (account_type_key, user_domain.lower(), user_login.lower()) data = { + 'Id': user.id, 'Domainname': user_domain, 'Username': user_login, 'AccountType': account_type, @@ -476,8 +477,9 @@ def build_group(group: ScimGroup) -> Optional[Tuple[admin_types.CollectionData, return None group_domain = group.domain or domain_name data = { + 'Id': group.id, + 'Domainname': group_domain, 'GroupName': group.name, - 'Domainname': group_domain } key = (group_domain.lower(), group.name.lower()) From 3f483d5fb241543e9b486463a90ba325b7dbdc4c Mon Sep 17 00:00:00 2001 From: Craig Date: Wed, 12 Aug 2026 21:44:50 -0700 Subject: [PATCH 09/13] Output full record contents for record-history -a view --format=json The view action previously emitted flattened Name/Value rows even in JSON format. It now emits the complete record contents in the same structure as the get command: raw decrypted record data for typed (v3+) records, and get-style field names (login, password, login_url, custom_fields, totp, attachments) for legacy records, plus revision metadata (version, revision, modified_by, client_modified_time). This allows raw history diffs between revisions. Table output is unchanged. Co-Authored-By: Claude Fable 5 --- keepercommander/commands/record.py | 57 +++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/keepercommander/commands/record.py b/keepercommander/commands/record.py index 2fa86f0df..1774a65d5 100644 --- a/keepercommander/commands/record.py +++ b/keepercommander/commands/record.py @@ -2736,6 +2736,62 @@ def execute(self, params, **kwargs): if action == 'view': rev = history[index] record = vault.KeeperRecord.load(params, rev) + fmt = kwargs.get('format') or '' + + if fmt == 'json': + version = rev.get('version') or 0 + ro = { + 'record_uid': record.record_uid + } + if version < 3: + ro['title'] = record.title + if isinstance(record, vault.PasswordRecord): + if record.login: + ro['login'] = record.login + if record.password: + ro['password'] = record.password + if record.link: + ro['login_url'] = record.link + if record.custom: + ro['custom_fields'] = [{ + 'name': x.name, + 'value': x.value, + 'type': x.type + } for x in record.custom] + if record.totp: + ro['totp'] = record.totp + if record.attachments: + ro['attachments'] = [{ + 'id': a.id, + 'name': a.name, + 'size': a.size + } for a in record.attachments] + if record.notes: + ro['notes'] = record.notes + else: + data = rev['data_unencrypted'] if 'data_unencrypted' in rev else b'{}' + data = json.loads(data.decode()) + ro.update(data) + ro['version'] = version + ro['revision'] = record.revision + if 'user_name' in rev: + ro['modified_by'] = rev['user_name'] + if 'client_modified_time' in rev: + cmt = rev['client_modified_time'] + if isinstance(cmt, (int, float)): + dt = datetime.datetime.fromtimestamp(int(cmt / 1000)) + ro['client_modified_time'] = dt.isoformat() + + output = kwargs.get('output') + if output: + _, ext = os.path.splitext(output) + if not ext: + output += '.json' + with open(output, 'w') as f: + json.dump(ro, f, indent=2) + else: + print(json.dumps(ro, indent=2)) + return rows = [] for name, value in record.enumerate_fields(): @@ -2747,7 +2803,6 @@ def execute(self, params, **kwargs): rows.append([name, value]) modified = datetime.datetime.fromtimestamp(int(rev['client_modified_time'] / 1000.0)) rows.append(['Modified', modified]) - fmt = kwargs.get('format') or '' return base.dump_report_data(rows, headers=['Name', 'Value'], title=f'Record Revision V.{revision}', no_header=True, right_align=(0,), fmt=fmt, filename=kwargs.get('output')) From 31f0c603e981fe6431cc4b18560b89eef90f0c85 Mon Sep 17 00:00:00 2001 From: sshrushanth-ks Date: Thu, 13 Aug 2026 23:54:39 +0530 Subject: [PATCH 10/13] KC-1393: Fix pam rbi edit so NSF RBI settings persist via update_pam_record (#2283) (#2287) * Fix pam rbi edit so NSF RBI settings persist via update_pam_record * Add NSF rbi edit regression test and debug log for is_pam_nsf_record fallback --- keepercommander/commands/pam/vault_target.py | 12 +- .../commands/tunnel_and_connections.py | 3 +- unit-tests/pam/test_pam_rbi_edit.py | 204 +++++++++--------- 3 files changed, 115 insertions(+), 104 deletions(-) diff --git a/keepercommander/commands/pam/vault_target.py b/keepercommander/commands/pam/vault_target.py index dae5e96cb..d30377e07 100644 --- a/keepercommander/commands/pam/vault_target.py +++ b/keepercommander/commands/pam/vault_target.py @@ -1,3 +1,5 @@ +import logging + from ..folder import FolderMoveCommand from ... import api, record_management, vault, vault_extensions from ...error import CommandError @@ -553,8 +555,14 @@ def is_pam_nsf_record(params, record_uid): for folder_uid in find_folders(params, record_uid): if is_nested_share_folder(params, folder_uid): return True - except Exception: - pass + except Exception as e: + # Fail open to the classic update path when folder lookup fails. + # NSF records missing from nested_share_* caches can then silently use + # classic update_record and appear not to persist. + logging.debug( + 'is_pam_nsf_record: folder lookup failed for record %s (%s); ' + 'treating as classic/non-NSF', + record_uid, e) return False diff --git a/keepercommander/commands/tunnel_and_connections.py b/keepercommander/commands/tunnel_and_connections.py index 32cb55b9a..60b90c59d 100644 --- a/keepercommander/commands/tunnel_and_connections.py +++ b/keepercommander/commands/tunnel_and_connections.py @@ -4055,8 +4055,7 @@ def update_connection_choice(field_name, value): update_connection_choice('sessionPersistence', session_persistence) if dirty: - record_management.update_record(params, record) - api.sync_down(params) + update_pam_record(params, record, command='pam rbi edit') traffic_encryption_key = record.get_typed_field('trafficEncryptionSeed') if not traffic_encryption_key: diff --git a/unit-tests/pam/test_pam_rbi_edit.py b/unit-tests/pam/test_pam_rbi_edit.py index d3a451b20..67e5ad155 100644 --- a/unit-tests/pam/test_pam_rbi_edit.py +++ b/unit-tests/pam/test_pam_rbi_edit.py @@ -250,142 +250,125 @@ def test_no_param_raises_error_with_new_settings_check(self): self.assertIn('At least one parameter is required', str(context.exception)) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_url_navigation_on_sets_true(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_url_navigation_on_sets_true(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_url_navigation='on') self.assertEqual(self.pam_settings['connection'].get('allowUrlManipulation'), True) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_url_navigation_off_sets_false(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_url_navigation_off_sets_false(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_url_navigation='off') self.assertEqual(self.pam_settings['connection'].get('allowUrlManipulation'), False) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_url_navigation_default_removes_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_url_navigation_default_removes_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.pam_settings['connection']['allowUrlManipulation'] = True self.command.execute(self.mock_params, record='test-record', allow_url_navigation='default') self.assertNotIn('allowUrlManipulation', self.pam_settings['connection']) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_ignore_server_cert_on_sets_true(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_ignore_server_cert_on_sets_true(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', ignore_server_cert='on') self.assertEqual(self.pam_settings['connection'].get('ignoreInitialSslCert'), True) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_file_uploads_on_sets_true(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_file_uploads_on_sets_true(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_file_uploads='on') self.assertEqual(self.pam_settings['connection'].get('allowFileUploads'), True) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_file_uploads_off_sets_false(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_file_uploads_off_sets_false(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_file_uploads='off') self.assertEqual(self.pam_settings['connection'].get('allowFileUploads'), False) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_file_uploads_default_removes_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_file_uploads_default_removes_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.pam_settings['connection']['allowFileUploads'] = True self.command.execute(self.mock_params, record='test-record', allow_file_uploads='default') self.assertNotIn('allowFileUploads', self.pam_settings['connection']) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_file_downloads_on_sets_true(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_file_downloads_on_sets_true(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_file_downloads='on') self.assertEqual(self.pam_settings['connection'].get('allowFileDownloads'), True) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_file_downloads_off_sets_false(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_file_downloads_off_sets_false(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_file_downloads='off') self.assertEqual(self.pam_settings['connection'].get('allowFileDownloads'), False) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_file_downloads_default_removes_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_file_downloads_default_removes_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.pam_settings['connection']['allowFileDownloads'] = True self.command.execute(self.mock_params, record='test-record', allow_file_downloads='default') self.assertNotIn('allowFileDownloads', self.pam_settings['connection']) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allowed_urls_joins_with_newlines(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allowed_urls_joins_with_newlines(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allowed_urls=['*.example.com', '*.test.com']) self.assertEqual(self.pam_settings['connection'].get('allowedUrlPatterns'), '*.example.com\n*.test.com') @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allowed_resource_urls_joins_with_newlines(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allowed_resource_urls_joins_with_newlines(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allowed_resource_urls=['*.cdn.example.com']) self.assertEqual(self.pam_settings['connection'].get('allowedResourceUrlPatterns'), '*.cdn.example.com') @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_autofill_targets_joins_with_newlines(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_autofill_targets_joins_with_newlines(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', autofill_targets=['#username', '#password']) self.assertEqual(self.pam_settings['connection'].get('autofillConfiguration'), '#username\n#password') @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_session_persistence_sets_value(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_session_persistence_sets_value(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', session_persistence='user') self.assertEqual(self.pam_settings['connection'].get('sessionPersistence'), 'user') @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_session_persistence_none_sets_literal(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_session_persistence_none_sets_literal(self, mock_update, mock_resolve): # 'none' is a real enum value (no persistence), not a removal sentinel mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', session_persistence='none') self.assertEqual(self.pam_settings['connection'].get('sessionPersistence'), 'none') @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_session_persistence_default_removes_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_session_persistence_default_removes_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.pam_settings['connection']['sessionPersistence'] = 'user' self.command.execute(self.mock_params, record='test-record', session_persistence='default') self.assertNotIn('sessionPersistence', self.pam_settings['connection']) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_session_persistence_default_removes_present_but_null(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_session_persistence_default_removes_present_but_null(self, mock_update, mock_resolve): # A present-but-null value must still be removed (membership check, not None check) mock_resolve.return_value = self.mock_record self.pam_settings['connection']['sessionPersistence'] = None @@ -409,59 +392,52 @@ def setUp(self): self.mock_params = mock.MagicMock() @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_copy_on_sets_disable_copy_false(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_copy_on_sets_disable_copy_false(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_copy='on') self.assertEqual(self.pam_settings['connection'].get('disableCopy'), False) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_copy_off_sets_disable_copy_true(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_copy_off_sets_disable_copy_true(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_copy='off') self.assertEqual(self.pam_settings['connection'].get('disableCopy'), True) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_copy_default_removes_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_copy_default_removes_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.pam_settings['connection']['disableCopy'] = True self.command.execute(self.mock_params, record='test-record', allow_copy='default') self.assertNotIn('disableCopy', self.pam_settings['connection']) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_paste_on_sets_disable_paste_false(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_paste_on_sets_disable_paste_false(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_paste='on') self.assertEqual(self.pam_settings['connection'].get('disablePaste'), False) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_allow_paste_off_sets_disable_paste_true(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_allow_paste_off_sets_disable_paste_true(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_paste='off') self.assertEqual(self.pam_settings['connection'].get('disablePaste'), True) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_clipboard_both_on(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_clipboard_both_on(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_copy='on', allow_paste='on') self.assertEqual(self.pam_settings['connection'].get('disableCopy'), False) self.assertEqual(self.pam_settings['connection'].get('disablePaste'), False) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_clipboard_both_off(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_clipboard_both_off(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_copy='off', allow_paste='off') self.assertEqual(self.pam_settings['connection'].get('disableCopy'), True) @@ -484,23 +460,58 @@ def setUp(self): self.mock_params = mock.MagicMock() @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_record_update_called_when_field_changes(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_record_update_called_when_field_changes(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_url_navigation='on') mock_update.assert_called_once() - mock_sync.assert_called_once() + args, kwargs = mock_update.call_args + self.assertEqual(kwargs.get('command'), 'pam rbi edit') @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_multiple_fields_single_update(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_multiple_fields_single_update(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', allow_url_navigation='on', ignore_server_cert='off', allow_copy='on', allowed_urls=['*.example.com']) mock_update.assert_called_once() + @mock.patch('keepercommander.commands.pam.vault_target.record_management.update_record') + @mock.patch('keepercommander.commands.pam_import.nsf_helpers.sync_down_preserving_nsf_keys') + @mock.patch('keepercommander.nested_share_folder.record_api.update_record_v3', + return_value={'success': True}) + @mock.patch('keepercommander.vault_extensions.extract_typed_record_data', + return_value={'type': 'pamRemoteBrowser', 'title': 'NSF RBI', 'fields': []}) + @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') + def test_nsf_rbi_edit_routes_to_update_record_v3(self, mock_resolve, _extract, mock_update_v3, + mock_nsf_sync, mock_classic_update): + """Regression: NSF pamRemoteBrowser edits must use NSF v3 update, not classic update_record.""" + record_uid = 'nsf-rbi-uid' + record = vault.TypedRecord(version=3) + record.record_uid = record_uid + record.type_name = 'pamRemoteBrowser' + record.title = 'NSF RBI' + record.fields.append(vault.TypedField.new_field('trafficEncryptionSeed', 'seed', '')) + record.fields.append(vault.TypedField.new_field( + 'pamRemoteBrowserSettings', + {'connection': {'protocol': 'http', 'sessionPersistence': 'resource'}}, + '')) + + params = mock.MagicMock() + params.nested_share_records = {record_uid: {'revision': 1}} + params.nested_share_record_data = {} + mock_resolve.return_value = record + + self.command.execute(params, record=record_uid, session_persistence='user') + + mock_update_v3.assert_called_once() + self.assertEqual(mock_update_v3.call_args.args[1], record_uid) + mock_nsf_sync.assert_called_once_with(params) + mock_classic_update.assert_not_called() + self.assertEqual( + record.get_typed_field('pamRemoteBrowserSettings').value[0]['connection']['sessionPersistence'], + 'user') + @unittest.skipIf(skip_tests, skip_reason) class TestPamRbiEditHelp(unittest.TestCase): @@ -616,25 +627,22 @@ def test_disable_audio_default(self): self.assertEqual(args.disable_audio, 'default') @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_disable_audio_on_sets_true(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_disable_audio_on_sets_true(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', disable_audio='on') self.assertEqual(self.pam_settings['connection'].get('disableAudio'), True) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_disable_audio_off_sets_false(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_disable_audio_off_sets_false(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', disable_audio='off') self.assertEqual(self.pam_settings['connection'].get('disableAudio'), False) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_disable_audio_default_removes_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_disable_audio_default_removes_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.pam_settings['connection']['disableAudio'] = True self.command.execute(self.mock_params, record='test-record', disable_audio='default') @@ -649,9 +657,8 @@ def test_audio_channels_mono(self): self.assertEqual(args.audio_channels, 1) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_audio_channels_sets_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_audio_channels_sets_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', audio_channels=2) self.assertEqual(self.pam_settings['connection'].get('audioChannels'), 2) @@ -669,9 +676,8 @@ def test_audio_bit_depth_invalid(self): self.parser.parse_args(['--record', 'test-record', '--audio-bit-depth', '24']) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_audio_bit_depth_sets_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_audio_bit_depth_sets_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', audio_bit_depth=16) self.assertEqual(self.pam_settings['connection'].get('audioBps'), 16) @@ -685,17 +691,15 @@ def test_audio_sample_rate_48000(self): self.assertEqual(args.audio_sample_rate, 48000) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_audio_sample_rate_sets_field(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_audio_sample_rate_sets_field(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', audio_sample_rate=48000) self.assertEqual(self.pam_settings['connection'].get('audioSampleRate'), 48000) @mock.patch('keepercommander.commands.tunnel_and_connections.RecordMixin.resolve_single_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.record_management.update_record') - @mock.patch('keepercommander.commands.tunnel_and_connections.api.sync_down') - def test_all_audio_settings_combined(self, mock_sync, mock_update, mock_resolve): + @mock.patch('keepercommander.commands.tunnel_and_connections.update_pam_record') + def test_all_audio_settings_combined(self, mock_update, mock_resolve): mock_resolve.return_value = self.mock_record self.command.execute(self.mock_params, record='test-record', disable_audio='off', audio_channels=2, audio_bit_depth=16, audio_sample_rate=44100) From 07602ab2a2e3dd77293fa2c0614c0b9b1b3b12b1 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 13 Aug 2026 23:56:36 +0530 Subject: [PATCH 11/13] Add team name validation for key fetching (#2282) --- .../commands/nested_share_folder/helpers.py | 31 +++++--- .../nested_share_folder/__init__.py | 2 +- keepercommander/nested_share_folder/common.py | 77 ++++++++++++------- unit-tests/test_nested_share_folder.py | 40 ++++++++++ 4 files changed, 113 insertions(+), 37 deletions(-) diff --git a/keepercommander/commands/nested_share_folder/helpers.py b/keepercommander/commands/nested_share_folder/helpers.py index 00ad50152..5b2718375 100644 --- a/keepercommander/commands/nested_share_folder/helpers.py +++ b/keepercommander/commands/nested_share_folder/helpers.py @@ -265,13 +265,16 @@ def classify_share_recipient(params, recipient): (cap of 500 entries), ``params.available_team_cache``, and ``resolve_team_identifier`` (``team_cache``). A match by team name *or* team UID returns ``('team', team_uid_b64)``. - 3. No match → logs the same warning as legacy and returns ``None``. - 4. Multiple matches → logs the same warning and returns ``None``. + 3. Unresolved Keeper UID-shaped strings are treated as account UIDs + (``('user', uid)``) — never as teams. + 4. Unresolved display names → logs a warning and returns ``None`` + (do not send them to ``team_get_keys``). + 5. Multiple team name matches → logs a warning and returns ``None``. Returns ``(kind, identifier)`` or ``None``. """ from ... import constants, api - from ...nested_share_folder.common import resolve_team_identifier + from ...nested_share_folder.common import is_keeper_uid, resolve_team_identifier if re.match(constants.EMAIL_PATTERN, recipient): return 'user', recipient.lower() @@ -296,16 +299,24 @@ def classify_share_recipient(params, recipient): if len(matches) == 1: return 'team', matches[0] - if not matches: - resolved_team = resolve_team_identifier(params, recipient) - if resolved_team: - return 'team', resolved_team[0] - logger.warning('User "%s" could not be resolved as email or team', - recipient) - else: + if matches: logger.warning( 'Multiple matches were found for team "%s". Try using its UID -- ' 'which can be found via `list-team` -- instead', recipient) + return None + + # Classic share-folder parity: resolve team by UID/name; do not treat + # unresolved display names as users (and never send them to team_get_keys). + resolved_team = resolve_team_identifier(params, recipient) + if resolved_team: + return 'team', resolved_team[0] + + if is_keeper_uid(recipient): + # Account UID (non-email user identifier) + return 'user', recipient + + logger.warning('User "%s" could not be resolved as email or team', + recipient) return None diff --git a/keepercommander/nested_share_folder/__init__.py b/keepercommander/nested_share_folder/__init__.py index 14c005919..e9d8ffe00 100644 --- a/keepercommander/nested_share_folder/__init__.py +++ b/keepercommander/nested_share_folder/__init__.py @@ -24,7 +24,7 @@ 'handle_share_invite', 'resolve_user_uid_bytes', 'load_user_public_key', 'parse_folder_access_result', 'resolve_team_uid_bytes', 'resolve_team_identifier', - 'get_team_keys', 'encrypt_for_team', + 'get_team_keys', 'encrypt_for_team', 'is_keeper_uid', ], 'folder_api': [ 'create_folder_data', 'encrypt_folder_key', diff --git a/keepercommander/nested_share_folder/common.py b/keepercommander/nested_share_folder/common.py index 261f47651..4bbdb561e 100644 --- a/keepercommander/nested_share_folder/common.py +++ b/keepercommander/nested_share_folder/common.py @@ -6,6 +6,7 @@ """ import logging +import re from typing import Optional, Dict, Any, Tuple from .. import utils, crypto, api @@ -14,6 +15,14 @@ logger = logging.getLogger(__name__) +# Keeper UIDs are 16 random bytes encoded as unpadded base64url (22 chars). +_KEEPER_UID_RE = re.compile(r'^[A-Za-z0-9_-]{22}$') + + +def is_keeper_uid(value: str) -> bool: + """True when *value* looks like a Keeper UID (not a display name).""" + return bool(value and _KEEPER_UID_RE.match(value)) + # ═══════════════════════════════════════════════════════════════════════════ # Key lookup helpers (previously copy-pasted 9+ / 3+ times) @@ -93,10 +102,9 @@ def encrypt_for_recipient(plaintext_key: bytes, public_key, use_ecc: bool) -> by # ═══════════════════════════════════════════════════════════════════════════ def resolve_user_uid_bytes(params, user_identifier: str) -> Optional[bytes]: - """Resolve an email or base64-url UID to raw UID bytes. + """Resolve a user email or account UID to raw account UID bytes. - Lookup order: user_cache → enterprise users → base64 decode. - Returns None when the identifier cannot be resolved. + Only UID-shaped strings are decoded — never display names. """ is_email = '@' in user_identifier @@ -114,56 +122,70 @@ def resolve_user_uid_bytes(params, user_identifier: str) -> Optional[bytes]: break return None - try: - return utils.base64_url_decode(user_identifier) - except Exception: - return None - + # Only treat UID-shaped strings as account UIDs — never decode display names. + if is_keeper_uid(user_identifier): + try: + return utils.base64_url_decode(user_identifier) + except Exception: + return None + return None def resolve_team_uid_bytes(params, team_identifier: str) -> Optional[bytes]: - """Resolve a team name or base64-url team UID to raw UID bytes. + """Resolve team UID or display name to team UID bytes. + + Matches classic share-folder: look up shareable + teams by UID or exact name. Does **not** base64-decode arbitrary strings — + that produced garbage UIDs (e.g. truncated names) and + ``bad_inputs_invalid_uid_string`` on ``team_get_keys``. """ - if not team_identifier: + identifier = (team_identifier or '').strip() + if not identifier: return None team_cache = getattr(params, 'team_cache', None) or {} - if team_identifier in team_cache: - return utils.base64_url_decode(team_identifier) - lower = team_identifier.lower() + if identifier in team_cache: + return utils.base64_url_decode(identifier) + lower = identifier.casefold() for uid, t in team_cache.items(): name = t.get('name') if isinstance(t, dict) else getattr(t, 'name', None) - if name and name.lower() == lower: + if name and name.casefold() == lower: return utils.base64_url_decode(uid) try: share_objects = api.get_share_objects(params).get('teams', {}) or {} except Exception: share_objects = {} - if team_identifier in share_objects: - return utils.base64_url_decode(team_identifier) + if identifier in share_objects: + return utils.base64_url_decode(identifier) for uid, t in share_objects.items(): name = (t.get('name') if isinstance(t, dict) else None) or '' - if name.lower() == lower: + if name and name.casefold() == lower: return utils.base64_url_decode(uid) - if len(share_objects) >= 500 or getattr(params, 'available_team_cache', None) is None: + # Enterprise teams not always present in get_share_objects — same as classic + # share-folder when the share-objects list is large / incomplete. + available = getattr(params, 'available_team_cache', None) + if len(share_objects) >= 500 or available is None: try: api.load_available_teams(params) + available = getattr(params, 'available_team_cache', None) except Exception: pass - for t in (getattr(params, 'available_team_cache', None) or []): + if not isinstance(available, (list, tuple)): + available = [] + for t in available: uid = t.get('team_uid') name = t.get('team_name', '') - if uid and (uid == team_identifier or name.lower() == lower): + if uid and (uid == identifier or (name and name.casefold() == lower)): return utils.base64_url_decode(uid) - try: - decoded = utils.base64_url_decode(team_identifier) - return decoded if decoded else None - except Exception: - return None - + if is_keeper_uid(identifier): + try: + return utils.base64_url_decode(identifier) + except Exception: + return None + return None def resolve_team_identifier(params, team_identifier: str) -> Optional[Tuple[str, bytes]]: """Resolve a team name/UID to ``(team_uid_b64, team_uid_bytes)`` or ``None``.""" @@ -178,6 +200,9 @@ def get_team_keys(params, team_uid_b64: str): """ from ..params import PublicKeys + if not is_keeper_uid(team_uid_b64): + raise ValueError(f'Invalid team UID: {team_uid_b64}') + cached = params.key_cache.get(team_uid_b64) has_asym = bool(cached and (getattr(cached, 'rsa', None) or getattr(cached, 'ec', None))) if cached and has_asym: diff --git a/unit-tests/test_nested_share_folder.py b/unit-tests/test_nested_share_folder.py index d48edcbf0..4d8555041 100644 --- a/unit-tests/test_nested_share_folder.py +++ b/unit-tests/test_nested_share_folder.py @@ -54,6 +54,9 @@ def _make_params(**overrides): p.nested_share_raw_dag_data = [] p.sync_data = False p.enterprise = None + p.team_cache = {} + p.available_team_cache = [] + p.key_cache = {} for k, v in overrides.items(): setattr(p, k, v) return p @@ -1311,6 +1314,43 @@ def test_classify_share_recipient_resolves_team_from_cache(self): result = classify_share_recipient(params, 'Ops Team') self.assertEqual(result, ('team', team_uid)) + def test_classify_share_recipient_rejects_unknown_team_name(self): + """Unknown display names must not be treated as users or fake team UIDs.""" + params = _make_params(team_cache={}) + with mock.patch('keepercommander.api.get_share_objects', return_value={'teams': {}}): + with mock.patch('keepercommander.api.load_available_teams'): + self.assertIsNone( + classify_share_recipient(params, 'Random Non existent team')) + # Base64-decodable short names previously became garbage team UIDs. + self.assertIsNone(classify_share_recipient(params, 'MyTeam')) + self.assertIsNone(classify_share_recipient(params, 'Ops')) + + def test_resolve_team_uid_bytes_does_not_decode_display_names(self): + from keepercommander.nested_share_folder.common import ( + resolve_team_uid_bytes, is_keeper_uid, get_team_keys) + + params = _make_params(team_cache={}) + with mock.patch('keepercommander.api.get_share_objects', return_value={'teams': {}}): + with mock.patch('keepercommander.api.load_available_teams'): + self.assertIsNone(resolve_team_uid_bytes(params, 'MyTeam')) + self.assertIsNone(resolve_team_uid_bytes(params, 'Random Non existent team')) + + team_uid = utils.generate_uid() + self.assertTrue(is_keeper_uid(team_uid)) + self.assertFalse(is_keeper_uid('MyTeam')) + self.assertFalse(is_keeper_uid('Random Non existent team')) + + params = _make_params(team_cache={ + team_uid: {'name': 'Ops Team', 'team_uid': team_uid}, + }) + with mock.patch('keepercommander.api.get_share_objects', return_value={'teams': {}}): + self.assertEqual( + resolve_team_uid_bytes(params, 'Ops Team'), + utils.base64_url_decode(team_uid)) + + with self.assertRaises(ValueError): + get_team_keys(params, 'MyTeam') + def test_parse_folder_access_result_treats_success_message_as_success(self): """SUCCESS status with a non-empty message is still treated as success.""" response = Mock() From 955537bb06f077fc895b038d2b087ca1c0f7acb4 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Thu, 13 Aug 2026 23:57:06 +0530 Subject: [PATCH 12/13] Increase timeout to be in sync with backend timeouts (#2288) --- keepercommander/rest_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keepercommander/rest_api.py b/keepercommander/rest_api.py index bae28ec21..8105b0221 100644 --- a/keepercommander/rest_api.py +++ b/keepercommander/rest_api.py @@ -131,7 +131,7 @@ def encrypt_with_keeper_key(context, data: bytes) -> bytes: raise KeeperApiError('invalid_key_id', f'Key ID \"{key_id}\" is not valid.') -DEFAULT_TIMEOUT = (15, 120) +DEFAULT_TIMEOUT = (15, 300) def execute_rest(context, endpoint, payload, timeout=None): From c8d98e7926107188d04480b223b4284cada134dd Mon Sep 17 00:00:00 2001 From: Matthew Ford Date: Fri, 14 Aug 2026 13:58:33 -0700 Subject: [PATCH 13/13] Updated krouter endpoint changes for GitHub USS configuration. Regenerated from proto. Fixed bug where pamGitHubConfiguration was not recognized as a PAM configuration type. Added tests. --- keepercommander/commands/discoveryrotation.py | 17 +- .../commands/discoveryrotation_v1.py | 14 +- .../commands/universalsecretsync.py | 193 +++++++++++++- keepercommander/proto/pam_pb2.py | 142 ++++++----- keepercommander/proto/pam_pb2.pyi | 119 ++++++++- unit-tests/pam/test_pam_nsf_config.py | 107 ++++++++ unit-tests/pam/test_uss_github_config.py | 239 ++++++++++++++++++ 7 files changed, 747 insertions(+), 84 deletions(-) create mode 100644 unit-tests/pam/test_uss_github_config.py diff --git a/keepercommander/commands/discoveryrotation.py b/keepercommander/commands/discoveryrotation.py index 028693d2f..29a2797d7 100644 --- a/keepercommander/commands/discoveryrotation.py +++ b/keepercommander/commands/discoveryrotation.py @@ -2566,6 +2566,7 @@ class PamConfigurationEditMixin(RecordEditMixin): PAM_CONFIG_RECORD_TYPES = frozenset({ 'pamAwsConfiguration', 'pamAzureConfiguration', 'pamGcpConfiguration', 'pamDomainConfiguration', 'pamNetworkConfiguration', 'pamOciConfiguration', + 'pamGitHubConfiguration', }) PAM_RESOURCE_RECORD_TYPES = frozenset({ 'pamDatabase', 'pamDirectory', 'pamMachine', 'pamRemoteBrowser', @@ -2758,7 +2759,7 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va extra_properties.append(f'text.pamGitHubId={github_id}') personal_access_token = kwargs.get('personal_access_token') if personal_access_token: - extra_properties.append(f'secret.personalAccessToken={personal_access_token}') + extra_properties.append(f'secret.pamGitHubPersonalAccessToken={personal_access_token}') github_base_url = kwargs.get('github_base_url') if github_base_url: extra_properties.append(f'text.pamGitHubBaseUrl={github_base_url}') @@ -2870,7 +2871,8 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va # Fields that the backend previously required but now treats as optional for pamAzureConfiguration. AZURE_OPTIONAL_FIELDS = frozenset({'clientId', 'clientSecret'}) - def verify_required(self, record): # type: (vault.TypedRecord) -> None + def verify_required(self, record, command=''): # type: (vault.TypedRecord, str) -> None + missing_fields = [] for field in record.fields: if field.required: if len(field.value) == 0: @@ -2882,10 +2884,15 @@ def verify_required(self, record): # type: (vault.TypedRecord) -> None and field.label in self.AZURE_OPTIONAL_FIELDS): pass else: - self.warnings.append(f'Empty required field: "{field.get_field_name()}"') + missing_fields.append(field.get_field_name()) for custom in record.custom: if custom.required: custom.required = False + if missing_fields: + if len(missing_fields) == 1: + raise CommandError(command, f'Empty required field: "{missing_fields[0]}"') + fields_text = ', '.join(f'"{x}"' for x in missing_fields) + raise CommandError(command, f'Empty required fields: {fields_text}') class PAMConfigurationNewCommand(Command, PamConfigurationEditMixin): @@ -2986,7 +2993,7 @@ def execute(self, params, **kwargs): if not gateway_uid: logging.warning(f'Gateway "{gw_name}" not found.') - self.verify_required(record) + self.verify_required(record, command='pam-config-new') create_pam_configuration_in_folder(params, record, shared_folder_uid, command='pam-config-new') @@ -3119,7 +3126,7 @@ def execute(self, params, **kwargs): orig_admin_cred_ref = value.get('adminCredentialRef') or '' self.parse_properties(params, configuration, config_edit=True, **kwargs) - self.verify_required(configuration) + self.verify_required(configuration, command='pam-config-edit') update_pam_record(params, configuration, command='pam-config-edit') diff --git a/keepercommander/commands/discoveryrotation_v1.py b/keepercommander/commands/discoveryrotation_v1.py index cadd5f0c9..8d2d21502 100644 --- a/keepercommander/commands/discoveryrotation_v1.py +++ b/keepercommander/commands/discoveryrotation_v1.py @@ -970,7 +970,8 @@ def parse_properties(self, params, record, **kwargs): # type: (KeeperParams, va if extra_properties: self.assign_typed_fields(record, [RecordEditMixin.parse_field(x) for x in extra_properties]) - def verify_required(self, record): # type: (vault.TypedRecord) -> None + def verify_required(self, record, command=''): # type: (vault.TypedRecord, str) -> None + missing_fields = [] for field in record.fields: if field.required: if len(field.value) == 0: @@ -981,10 +982,15 @@ def verify_required(self, record): # type: (vault.TypedRecord) -> None 'tz': 'Etc/UTC', }] else: - self.warnings.append(f'Empty required field: "{field.get_field_name()}"') + missing_fields.append(field.get_field_name()) for custom in record.custom: if custom.required: custom.required = False + if missing_fields: + if len(missing_fields) == 1: + raise CommandError(command, f'Empty required field: "{missing_fields[0]}"') + fields_text = ', '.join(f'"{x}"' for x in missing_fields) + raise CommandError(command, f'Empty required fields: {fields_text}') class PAMConfigurationNewCommand(Command, PamConfigurationEditMixin): @@ -1037,7 +1043,7 @@ def execute(self, params, **kwargs): if not shared_folder_uid: raise CommandError('pam-config-new', '--shared_folder parameter is required to create a PAM configuration') - self.verify_required(record) + self.verify_required(record, command='pam-config-new') pam_configuration_create_record_v6(params, record, shared_folder_uid) @@ -1126,7 +1132,7 @@ def execute(self, params, **kwargs): orig_shared_folder_uid = value.get('folderUid') or '' self.parse_properties(params, configuration, **kwargs) - self.verify_required(configuration) + self.verify_required(configuration, command='pam-config-edit') record_management.update_record(params, configuration) diff --git a/keepercommander/commands/universalsecretsync.py b/keepercommander/commands/universalsecretsync.py index 3dddfacfb..5bbda109e 100644 --- a/keepercommander/commands/universalsecretsync.py +++ b/keepercommander/commands/universalsecretsync.py @@ -78,7 +78,8 @@ def print_uss_configurations_list(params, format_type='table'): configurations = list(vault_extensions.find_records(params, record_version=6)) # Only process these specific configuration types - uss_supported_types = ('pamGcpConfiguration', 'pamAzureConfiguration', 'pamAwsConfiguration') + uss_supported_types = ('pamGcpConfiguration', 'pamAzureConfiguration', 'pamAwsConfiguration', + 'pamGitHubConfiguration') configs_data = [] for record in configurations: @@ -111,6 +112,9 @@ def print_uss_configurations_list(params, format_type='table'): # Get the configuration data config_data = universal_sync_edge.content_as_dict or {} + # GitHub-specific fields are nested under the 'github' key. + github_data = config_data.get('github') or {} + # Decrypt vault_name if present. The router stores it under the # 'vaultName' key as a base64-url string of the encrypted bytes. vault_name = 'N/A' @@ -143,6 +147,44 @@ def print_uss_configurations_list(params, format_type='table'): logging.debug(f"Failed to decrypt sync_identity for record {record.record_uid}: {e}") sync_identity = 'N/A' + # Decrypt owner if present. The router stores it under the + # 'github.owner' key as a base64-url string of the encrypted bytes. + owner = 'N/A' + owner_encrypted = github_data.get('owner') + if owner_encrypted: + try: + owner_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(owner_encrypted), record.record_key) + owner = owner_bytes.decode('utf-8') + except Exception as e: + logging.debug(f"Failed to decrypt owner for record {record.record_uid}: {e}") + owner = 'N/A' + + # scope/organizationVisibility are stored unencrypted (plain enum values) + scope_value = github_data.get('scope') + try: + scope_str = pam_pb2.GitHubScope.Name(scope_value) if scope_value is not None else 'N/A' + except ValueError: + scope_str = 'N/A' + + org_visibility_value = github_data.get('organizationVisibility') + try: + org_visibility_str = pam_pb2.GitHubOrganizationVisibility.Name(org_visibility_value) \ + if org_visibility_value is not None else 'N/A' + except ValueError: + org_visibility_str = 'N/A' + + # Decrypt GitHub repository names if present. The router stores them under + # the 'github.repos' key as a list of base64-url strings of the encrypted bytes. + repo_names = [] + for repo_encrypted in github_data.get('repos', []): + try: + repo_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(repo_encrypted), record.record_key) + repo_names.append(repo_bytes.decode('utf-8')) + except Exception as e: + logging.debug(f"Failed to decrypt repo name for record {record.record_uid}: {e}") + # Get folder UIDs - folders connected via universal_sync_folder (simple count only) folder_count = 0 for vertex in root.has_vertices(): @@ -160,6 +202,10 @@ def print_uss_configurations_list(params, format_type='table'): 'folder_count': folder_count, 'vault_name': vault_name, 'sync_identity': sync_identity, + 'scope': scope_str, + 'owner': owner, + 'organization_visibility': org_visibility_str, + 'repos': repo_names, }) except Exception as e: # Skip records that fail to load or don't have USS config @@ -180,13 +226,16 @@ def print_uss_configurations_list(params, format_type='table'): # Display as simple summary table table = [] - headers = ['Network UID', 'Title', 'Type', 'Enabled', 'Dry Run', 'Folders', 'Vault Name', 'Sync Identity'] + headers = ['Network UID', 'Title', 'Type', 'Enabled', 'Dry Run', 'Folders', 'Vault Name', 'Sync Identity', + 'Scope', 'Owner', 'Org Visibility', 'Repos'] for config in configs_data: enabled_str = f"{bcolors.OKGREEN}Yes{bcolors.ENDC}" if config['enabled'] else f"{bcolors.FAIL}No{bcolors.ENDC}" dry_run_str = f"{bcolors.WARNING}Yes{bcolors.ENDC}" if config['dry_run_enabled'] else "No" folder_count = config.get('folder_count', 0) folders_str = f"{folder_count} folder(s)" if folder_count > 0 else "None" + repos = config.get('repos', []) + repos_str = f"{len(repos)} repo(s)" if repos else "None" row = [ config['record_uid'], @@ -196,7 +245,11 @@ def print_uss_configurations_list(params, format_type='table'): dry_run_str, folders_str, config['vault_name'], - config['sync_identity'] + config['sync_identity'], + config.get('scope', 'N/A'), + config.get('owner', 'N/A'), + config.get('organization_visibility', 'N/A'), + repos_str ] table.append(row) @@ -219,7 +272,8 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): raise CommandError('', f'{bcolors.FAIL}Network "{network_uid}" not found{bcolors.ENDC}') # Check if it's a supported USS configuration type - uss_supported_types = ('pamGcpConfiguration', 'pamAzureConfiguration', 'pamAwsConfiguration') + uss_supported_types = ('pamGcpConfiguration', 'pamAzureConfiguration', 'pamAwsConfiguration', + 'pamGitHubConfiguration') if not isinstance(network, vault.TypedRecord) or network.record_type not in uss_supported_types: if format_type == 'json': return json.dumps({"error": f'Record "{network_uid}" is not a USS configuration'}) @@ -253,6 +307,9 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): # Get the configuration data config_data = universal_sync_edge.content_as_dict or {} + # GitHub-specific fields are nested under the 'github' key. + github_data = config_data.get('github') or {} + # Decrypt vault_name if present. The router stores it under the # 'vaultName' key as a base64-url string of the encrypted bytes. vault_name = 'N/A' @@ -285,6 +342,44 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): logging.debug(f"Failed to decrypt sync_identity for network {network.record_uid}: {e}") sync_identity = 'N/A' + # Decrypt owner if present. The router stores it under the + # 'github.owner' key as a base64-url string of the encrypted bytes. + owner = 'N/A' + owner_encrypted = github_data.get('owner') + if owner_encrypted: + try: + owner_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(owner_encrypted), network.record_key) + owner = owner_bytes.decode('utf-8') + except Exception as e: + logging.debug(f"Failed to decrypt owner for network {network.record_uid}: {e}") + owner = 'N/A' + + # scope/organizationVisibility are stored unencrypted (plain enum values) + scope_value = github_data.get('scope') + try: + scope_str = pam_pb2.GitHubScope.Name(scope_value) if scope_value is not None else 'N/A' + except ValueError: + scope_str = 'N/A' + + org_visibility_value = github_data.get('organizationVisibility') + try: + org_visibility_str = pam_pb2.GitHubOrganizationVisibility.Name(org_visibility_value) \ + if org_visibility_value is not None else 'N/A' + except ValueError: + org_visibility_str = 'N/A' + + # Decrypt GitHub repository names if present. The router stores them under + # the 'github.repos' key as a list of base64-url strings of the encrypted bytes. + repo_names = [] + for repo_encrypted in github_data.get('repos', []): + try: + repo_bytes = crypto.decrypt_aes_v2( + utils.base64_url_decode(repo_encrypted), network.record_key) + repo_names.append(repo_bytes.decode('utf-8')) + except Exception as e: + logging.debug(f"Failed to decrypt repo name for network {network.record_uid}: {e}") + # Get folder vertices with sync status folder_vertices = [] for vertex in root.has_vertices(): @@ -339,6 +434,10 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): 'dry_run_enabled': config_data.get('dryRunEnabled', False), 'vault_name': vault_name, 'sync_identity': sync_identity, + 'scope': scope_str, + 'owner': owner, + 'organization_visibility': org_visibility_str, + 'repos': repo_names, 'folders': [] } @@ -370,6 +469,10 @@ def print_uss_configuration_details(params, network_uid, format_type='table'): table.append(['Dry Run', 'Yes' if config_data.get('dryRunEnabled', False) else 'No']) table.append(['Vault Name', vault_name]) table.append(['Sync Identity', sync_identity]) + table.append(['Scope', scope_str]) + table.append(['Owner', owner]) + table.append(['Org Visibility', org_visibility_str]) + table.append(['Repos', ', '.join(repo_names) if repo_names else 'None']) table.append(['', '']) # Blank row separator # Display folder sync details @@ -430,6 +533,16 @@ class PAMUniversalSyncConfigAddCommand(Command): help='Identity record UID to use for syncing') parser.add_argument('--vault-name', '-vn', dest='vault_name', action='store', help='Vault name for universal sync') + parser.add_argument('--scope', '-sc', dest='scope', action='store', + choices=['repository', 'organization'], + help='GitHub sync scope: a single repository or an entire organization') + parser.add_argument('--owner', '-o', dest='owner', action='store', + help='GitHub organization or user name that owns the repositories') + parser.add_argument('--org-visibility', '-ov', dest='org_visibility', action='store', + choices=['all', 'private', 'selected'], + help='Repository visibility to sync when scope is organization') + parser.add_argument('--repo', '-r', dest='repo', action='append', + help='GitHub repository name to sync (can be specified multiple times; scope must be repository)') def get_parser(self): return PAMUniversalSyncConfigAddCommand.parser @@ -480,6 +593,29 @@ def execute(self, params, **kwargs): encrypted_vault_name = crypto.encrypt_aes_v2(vault_name_bytes, network.record_key) rq.vaultName = encrypted_vault_name + scope = kwargs.get('scope') + if scope is not None: + rq.github.scope = pam_pb2.ORGANIZATION if scope == 'organization' else pam_pb2.REPOSITORY + + owner = kwargs.get('owner') + if owner: + owner_bytes = string_to_bytes(owner) + encrypted_owner = crypto.encrypt_aes_v2(owner_bytes, network.record_key) + rq.github.owner = encrypted_owner + + org_visibility = kwargs.get('org_visibility') + if org_visibility is not None: + org_visibility_map = {'all': pam_pb2.ALL, 'private': pam_pb2.PRIVATE, 'selected': pam_pb2.SELECTED} + rq.github.organizationVisibility = org_visibility_map[org_visibility] + + repos = kwargs.get('repo') + if repos: + for repo_name in repos: + repo_bytes = string_to_bytes(repo_name) + repo_obj = pam_pb2.GitHubRepository() + repo_obj.name = crypto.encrypt_aes_v2(repo_bytes, network.record_key) + rq.github.repos.append(repo_obj) + encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(params) try: @@ -505,6 +641,16 @@ class PAMUniversalSyncConfigEditCommand(Command): help='Identity record UID to use for syncing') parser.add_argument('--vault-name', '-vn', dest='vault_name', action='store', help='Vault name for universal sync') + parser.add_argument('--scope', '-sc', dest='scope', action='store', + choices=['repository', 'organization'], + help='GitHub sync scope: a single repository or an entire organization') + parser.add_argument('--owner', '-o', dest='owner', action='store', + help='GitHub organization or user name that owns the repositories') + parser.add_argument('--org-visibility', '-ov', dest='org_visibility', action='store', + choices=['all', 'private', 'selected'], + help='Repository visibility to sync when scope is organization') + parser.add_argument('--repo', '-r', dest='repo', action='append', + help='GitHub repository name to sync (can be specified multiple times; scope must be repository)') def get_parser(self): return PAMUniversalSyncConfigEditCommand.parser @@ -601,6 +747,45 @@ def execute(self, params, **kwargs): elif existing_config.get('vaultName'): rq.vaultName = utils.base64_url_decode(existing_config['vaultName']) + # GitHub-specific fields live under the nested 'github' object, both in the + # request message (rq.github) and in the existing DAG edge content. + existing_github = existing_config.get('github') or {} + + scope = kwargs.get('scope') + if scope is not None: + rq.github.scope = pam_pb2.ORGANIZATION if scope == 'organization' else pam_pb2.REPOSITORY + elif 'scope' in existing_github: + rq.github.scope = existing_github['scope'] + + owner = kwargs.get('owner') + if owner: + owner_bytes = string_to_bytes(owner) + encrypted_owner = crypto.encrypt_aes_v2(owner_bytes, network.record_key) + rq.github.owner = encrypted_owner + elif existing_github.get('owner'): + rq.github.owner = utils.base64_url_decode(existing_github['owner']) + + org_visibility = kwargs.get('org_visibility') + if org_visibility is not None: + org_visibility_map = {'all': pam_pb2.ALL, 'private': pam_pb2.PRIVATE, 'selected': pam_pb2.SELECTED} + rq.github.organizationVisibility = org_visibility_map[org_visibility] + elif 'organizationVisibility' in existing_github: + rq.github.organizationVisibility = existing_github['organizationVisibility'] + + # Handle repos - if provided, replace all; if not, keep existing (re-encrypted values carried forward as-is) + repos = kwargs.get('repo') + if repos: + for repo_name in repos: + repo_bytes = string_to_bytes(repo_name) + repo_obj = pam_pb2.GitHubRepository() + repo_obj.name = crypto.encrypt_aes_v2(repo_bytes, network.record_key) + rq.github.repos.append(repo_obj) + else: + for existing_repo in existing_github.get('repos', []): + repo_obj = pam_pb2.GitHubRepository() + repo_obj.name = utils.base64_url_decode(existing_repo) + rq.github.repos.append(repo_obj) + encrypted_session_token, encrypted_transmission_key, transmission_key = get_keeper_tokens(params) try: diff --git a/keepercommander/proto/pam_pb2.py b/keepercommander/proto/pam_pb2.py index 05f9d0a0c..b773f6c8e 100644 --- a/keepercommander/proto/pam_pb2.py +++ b/keepercommander/proto/pam_pb2.py @@ -26,7 +26,7 @@ from . import record_pb2 as record__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tpam.proto\x12\x03PAM\x1a\x10\x65nterprise.proto\x1a\x0crecord.proto\"\x83\x01\n\x13PAMRotationSchedule\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x63onfigurationUid\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x14\n\x0cscheduleData\x18\x04 \x01(\t\x12\x12\n\nnoSchedule\x18\x05 \x01(\x08\"K\n\x1cPAMRotationSchedulesResponse\x12+\n\tschedules\x18\x01 \x03(\x0b\x32\x18.PAM.PAMRotationSchedule\"\x94\x01\n\x13PAMOnlineController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63onnectedOn\x18\x02 \x01(\x03\x12\x11\n\tipAddress\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\x12-\n\x0b\x63onnections\x18\x05 \x03(\x0b\x32\x18.PAM.PAMWebRtcConnection\"\xa7\x01\n\x13PAMWebRtcConnection\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12\'\n\x04type\x18\x02 \x01(\x0e\x32\x19.PAM.WebRtcConnectionType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08userName\x18\x04 \x01(\t\x12\x11\n\tstartedOn\x18\x05 \x01(\x03\x12\x18\n\x10\x63onfigurationUid\x18\x06 \x01(\x0c\"Y\n\x14PAMOnlineControllers\x12\x12\n\ndeprecated\x18\x01 \x03(\x0c\x12-\n\x0b\x63ontrollers\x18\x02 \x03(\x0b\x32\x18.PAM.PAMOnlineController\"9\n\x10PAMRotateRequest\x12\x12\n\nrequestUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\"A\n\x16PAMControllersResponse\x12\'\n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x12.PAM.PAMController\"=\n\x13PAMRemoveController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\t\"L\n\x1bPAMRemoveControllerResponse\x12-\n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x18.PAM.PAMRemoveController\"=\n\x10PAMModifyRequest\x12)\n\noperations\x18\x01 \x03(\x0b\x32\x15.PAM.PAMDataOperation\"\x98\x01\n\x10PAMDataOperation\x12,\n\roperationType\x18\x01 \x01(\x0e\x32\x15.PAM.PAMOperationType\x12\x30\n\rconfiguration\x18\x02 \x01(\x0b\x32\x19.PAM.PAMConfigurationData\x12$\n\x07\x65lement\x18\x03 \x01(\x0b\x32\x13.PAM.PAMElementData\"e\n\x14PAMConfigurationData\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\"E\n\x0ePAMElementData\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12\x11\n\tparentUid\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"m\n\x19PAMElementOperationResult\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12+\n\x06result\x18\x02 \x01(\x0e\x32\x1b.PAM.PAMOperationResultType\x12\x0f\n\x07message\x18\x03 \x01(\t\"B\n\x0fPAMModifyResult\x12/\n\x07results\x18\x01 \x03(\x0b\x32\x1e.PAM.PAMElementOperationResult\"x\n\nPAMElement\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x03 \x01(\x03\x12\x14\n\x0clastModified\x18\x04 \x01(\x03\x12!\n\x08\x63hildren\x18\x05 \x03(\x0b\x32\x0f.PAM.PAMElement\"#\n\x14PAMGenericUidRequest\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\"%\n\x15PAMGenericUidsRequest\x12\x0c\n\x04uids\x18\x01 \x03(\x0c\"\xab\x01\n\x10PAMConfiguration\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x05 \x01(\x03\x12\x14\n\x0clastModified\x18\x06 \x01(\x03\x12!\n\x08\x63hildren\x18\x07 \x03(\x0b\x32\x0f.PAM.PAMElement\"B\n\x11PAMConfigurations\x12-\n\x0e\x63onfigurations\x18\x01 \x03(\x0b\x32\x15.PAM.PAMConfiguration\"\xff\x01\n\rPAMController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63ontrollerName\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65viceToken\x18\x03 \x01(\t\x12\x12\n\ndeviceName\x18\x04 \x01(\t\x12\x0e\n\x06nodeId\x18\x05 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x06 \x01(\x03\x12\x14\n\x0clastModified\x18\x07 \x01(\x03\x12\x16\n\x0e\x61pplicationUid\x18\x08 \x01(\x0c\x12\x30\n\rappClientType\x18\t \x01(\x0e\x32\x19.Enterprise.AppClientType\x12\x15\n\risInitialized\x18\n \x01(\x08\"P\n\x1dPAMSetMaxInstanceCountRequest\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x18\n\x10maxInstanceCount\x18\x02 \x01(\x05\"%\n\x12\x43ontrollerResponse\x12\x0f\n\x07payload\x18\x01 \x01(\t\"M\n\x1aPAMConfigurationController\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x02 \x01(\x0c\"\xa3\x01\n\x17\x43onfigurationAddRequest\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x11\n\trecordKey\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12(\n\x0brecordLinks\x18\x04 \x03(\x0b\x32\x13.Records.RecordLink\x12#\n\x05\x61udit\x18\x05 \x01(\x0b\x32\x14.Records.RecordAudit\"J\n\x10RelayAccessCreds\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x12\n\nserverTime\x18\x03 \x01(\x03\"\x81\x02\n\x14PAMRecordingsRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08maxCount\x18\x02 \x01(\x05\x12\x17\n\nrangeStart\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x15\n\x08rangeEnd\x18\x04 \x01(\x03H\x01\x88\x01\x01\x12$\n\x05types\x18\x05 \x03(\x0e\x32\x15.PAM.PAMRecordingType\x12)\n\x05risks\x18\x06 \x03(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x11\n\tprotocols\x18\x07 \x03(\t\x12\x14\n\x0c\x63loseReasons\x18\x08 \x03(\x05\x42\r\n\x0b_rangeStartB\x0b\n\t_rangeEnd\"\x89\x02\n\x1cPAMRecordingsForUsersRequest\x12\x11\n\tusernames\x18\x01 \x03(\t\x12\x10\n\x08maxCount\x18\x02 \x01(\x05\x12\x17\n\nrangeStart\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x15\n\x08rangeEnd\x18\x04 \x01(\x03H\x01\x88\x01\x01\x12$\n\x05types\x18\x05 \x03(\x0e\x32\x15.PAM.PAMRecordingType\x12)\n\x05risks\x18\x06 \x03(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x11\n\tprotocols\x18\x07 \x03(\t\x12\x14\n\x0c\x63loseReasons\x18\x08 \x03(\x05\x42\r\n\x0b_rangeStartB\x0b\n\t_rangeEnd\"\xd4\x02\n\x0cPAMRecording\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12,\n\rrecordingType\x18\x02 \x01(\x0e\x32\x15.PAM.PAMRecordingType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08userName\x18\x04 \x01(\t\x12\x11\n\tstartedOn\x18\x05 \x01(\x03\x12\x0e\n\x06length\x18\x06 \x01(\x05\x12\x10\n\x08\x66ileSize\x18\x07 \x01(\x03\x12\x11\n\tcreatedOn\x18\x08 \x01(\x03\x12\x10\n\x08protocol\x18\t \x01(\t\x12\x13\n\x0b\x63loseReason\x18\n \x01(\x05\x12\x19\n\x11recordingDuration\x18\x0b \x01(\x05\x12\x36\n\x12\x61iOverallRiskLevel\x18\x0c \x01(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x18\n\x10\x61iOverallSummary\x18\r \x01(\x0c\"O\n\x15PAMRecordingsResponse\x12%\n\nrecordings\x18\x01 \x03(\x0b\x32\x11.PAM.PAMRecording\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\"*\n\x07PAMData\x12\x0e\n\x06vertex\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c\"\x17\n\x07UidList\x12\x0c\n\x04uids\x18\x01 \x03(\x0c\"\x84\x03\n\x11PAMResourceConfig\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\nnetworkUid\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\x15\n\x08\x61\x64minUid\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x12\x11\n\x04meta\x18\x04 \x01(\x0cH\x02\x88\x01\x01\x12\x1f\n\x12\x63onnectionSettings\x18\x05 \x01(\x0cH\x03\x88\x01\x01\x12\'\n\x0c\x63onnectUsers\x18\x06 \x01(\x0b\x32\x0c.PAM.UidListH\x04\x88\x01\x01\x12\x16\n\tdomainUid\x18\x07 \x01(\x0cH\x05\x88\x01\x01\x12\x18\n\x0bjitSettings\x18\x08 \x01(\x0cH\x06\x88\x01\x01\x12\x1d\n\x10keeperAiSettings\x18\t \x01(\x0cH\x07\x88\x01\x01\x42\r\n\x0b_networkUidB\x0b\n\t_adminUidB\x07\n\x05_metaB\x15\n\x13_connectionSettingsB\x0f\n\r_connectUsersB\x0c\n\n_domainUidB\x0e\n\x0c_jitSettingsB\x13\n\x11_keeperAiSettings\"%\n\x16PAMUniversalSyncFolder\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\"\xfc\x01\n\x16PAMUniversalSyncConfig\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x14\n\x07\x65nabled\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1a\n\rdryRunEnabled\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12,\n\x07\x66olders\x18\x04 \x03(\x0b\x32\x1b.PAM.PAMUniversalSyncFolder\x12\x19\n\x0csyncIdentity\x18\x05 \x01(\x0cH\x02\x88\x01\x01\x12\x16\n\tvaultName\x18\x06 \x01(\x0cH\x03\x88\x01\x01\x42\n\n\x08_enabledB\x10\n\x0e_dryRunEnabledB\x0f\n\r_syncIdentityB\x0c\n\n_vaultName\"7\n\x11NhiMetricsRequest\x12\x11\n\tstartTime\x18\x01 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x02 \x01(\x03\"\x9c\x02\n\x0ePamUsageByUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12!\n\x19recordRotationScheduledOk\x18\x02 \x01(\x05\x12\x1c\n\x14pamConnectionStarted\x18\x03 \x01(\x05\x12\x18\n\x10pamTunnelStarted\x18\x04 \x01(\x05\x12\x1b\n\x13\x64iscoveryJobStarted\x18\x05 \x01(\x05\x12 \n\x18recordRotationOnDemandOk\x18\x06 \x01(\x05\x12\"\n\x1apamSessionRecordingStarted\x18\x07 \x01(\x05\x12\x15\n\rpamRbiStarted\x18\x08 \x01(\x05\x12%\n\x1dpamSessionRbiRecordingStarted\x18\t \x01(\x05\"p\n\x0eNhiUsageByUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\trotations\x18\x02 \x01(\x05\x12\x0f\n\x07tunnels\x18\x03 \x01(\x05\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x15\n\rdiscoveryJobs\x18\x05 \x01(\x05\"\x84\x02\n\x12NhiMetricsResponse\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x03\x12\x11\n\tstartTime\x18\x02 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x03 \x01(\x03\x12\x18\n\x10uniqueKsmDevices\x18\x04 \x01(\x05\x12\x18\n\x10pamGatewayOnline\x18\x05 \x01(\x05\x12/\n\x0epamUsageByUser\x18\x06 \x03(\x0b\x32\x13.PAM.PamUsageByUserB\x02\x18\x01\x12\x10\n\x08nhiCount\x18\x07 \x01(\x05\x12\x13\n\x0bksmNhiCount\x18\x08 \x01(\x05\x12(\n\x0busageByUser\x18\t \x03(\x0b\x32\x13.PAM.NhiUsageByUser\"D\n\x16NhiBulkMetricsResponse\x12*\n\tresponses\x18\x01 \x03(\x0b\x32\x17.PAM.NhiMetricsResponse\"N\n\x0bNhiUidEntry\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\"\n\x08\x63\x61tegory\x18\x02 \x01(\x0e\x32\x10.PAM.NhiCategory\x12\x0e\n\x06ksmNhi\x18\x03 \x01(\x08\"7\n\x11GetNhiUidsRequest\x12\x11\n\tstartTime\x18\x01 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x02 \x01(\x03\"4\n\x12GetNhiUidsResponse\x12\x1e\n\x04uids\x18\x01 \x03(\x0b\x32\x10.PAM.NhiUidEntry*\x9e\x01\n\x14WebRtcConnectionType\x12\x0e\n\nCONNECTION\x10\x00\x12\n\n\x06TUNNEL\x10\x01\x12\x07\n\x03SSH\x10\x02\x12\x07\n\x03RDP\x10\x03\x12\x08\n\x04HTTP\x10\x04\x12\x07\n\x03VNC\x10\x05\x12\n\n\x06TELNET\x10\x06\x12\t\n\x05MYSQL\x10\x07\x12\x0e\n\nSQL_SERVER\x10\x08\x12\x0e\n\nPOSTGRESQL\x10\t\x12\x0e\n\nKUBERNETES\x10\n*@\n\x10PAMOperationType\x12\x07\n\x03\x41\x44\x44\x10\x00\x12\n\n\x06UPDATE\x10\x01\x12\x0b\n\x07REPLACE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03*p\n\x16PAMOperationResultType\x12\x0f\n\x0bPOT_SUCCESS\x10\x00\x12\x15\n\x11POT_UNKNOWN_ERROR\x10\x01\x12\x16\n\x12POT_ALREADY_EXISTS\x10\x02\x12\x16\n\x12POT_DOES_NOT_EXIST\x10\x03*\xc9\x01\n\x15\x43ontrollerMessageType\x12\x0f\n\x0b\x43MT_GENERAL\x10\x00\x12\x0e\n\nCMT_ROTATE\x10\x01\x12\x11\n\rCMT_DISCOVERY\x10\x02\x12\x0f\n\x0b\x43MT_CONNECT\x10\x03\x12\x19\n\x15\x43MT_ANALYZE_RECORDING\x10\x04\x12!\n\x1d\x43MT_WORKFLOW_ACCESS_ELEVATION\x10\x05\x12\x0b\n\x07\x43MT_USS\x10\x06\x12\x0c\n\x08\x43MT_INFO\x10\x07\x12\x12\n\x0e\x43MT_AUTOMATION\x10\x08*V\n\x10PAMRecordingType\x12\x0f\n\x0bPRT_SESSION\x10\x00\x12\x12\n\x0ePRT_TYPESCRIPT\x10\x01\x12\x0c\n\x08PRT_TIME\x10\x02\x12\x0f\n\x0bPRT_SUMMARY\x10\x03*i\n\x15PAMRecordingRiskLevel\x12\x13\n\x0fPRR_UNSPECIFIED\x10\x00\x12\x0b\n\x07PRR_LOW\x10\x01\x12\x0e\n\nPRR_MEDIUM\x10\x02\x12\x0c\n\x08PRR_HIGH\x10\x03\x12\x10\n\x0cPRR_CRITICAL\x10\x04*`\n\x0bNhiCategory\x12\x18\n\x14NHI_CATEGORY_UNKNOWN\x10\x00\x12\x0c\n\x08PAM_USER\x10\x01\x12\x10\n\x0cPAM_RESOURCE\x10\x02\x12\x0b\n\x07GATEWAY\x10\x03\x12\n\n\x06\x44\x45VICE\x10\x04\x42\x1f\n\x18\x63om.keepersecurity.protoB\x03PAMb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tpam.proto\x12\x03PAM\x1a\x10\x65nterprise.proto\x1a\x0crecord.proto\"\x83\x01\n\x13PAMRotationSchedule\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x18\n\x10\x63onfigurationUid\x18\x02 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x14\n\x0cscheduleData\x18\x04 \x01(\t\x12\x12\n\nnoSchedule\x18\x05 \x01(\x08\"K\n\x1cPAMRotationSchedulesResponse\x12+\n\tschedules\x18\x01 \x03(\x0b\x32\x18.PAM.PAMRotationSchedule\"\x94\x01\n\x13PAMOnlineController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63onnectedOn\x18\x02 \x01(\x03\x12\x11\n\tipAddress\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\x12-\n\x0b\x63onnections\x18\x05 \x03(\x0b\x32\x18.PAM.PAMWebRtcConnection\"\xa7\x01\n\x13PAMWebRtcConnection\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12\'\n\x04type\x18\x02 \x01(\x0e\x32\x19.PAM.WebRtcConnectionType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08userName\x18\x04 \x01(\t\x12\x11\n\tstartedOn\x18\x05 \x01(\x03\x12\x18\n\x10\x63onfigurationUid\x18\x06 \x01(\x0c\"Y\n\x14PAMOnlineControllers\x12\x12\n\ndeprecated\x18\x01 \x03(\x0c\x12-\n\x0b\x63ontrollers\x18\x02 \x03(\x0b\x32\x18.PAM.PAMOnlineController\"9\n\x10PAMRotateRequest\x12\x12\n\nrequestUid\x18\x01 \x01(\x0c\x12\x11\n\trecordUid\x18\x02 \x01(\x0c\"A\n\x16PAMControllersResponse\x12\'\n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x12.PAM.PAMController\"=\n\x13PAMRemoveController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x0f\n\x07message\x18\x02 \x01(\t\"L\n\x1bPAMRemoveControllerResponse\x12-\n\x0b\x63ontrollers\x18\x01 \x03(\x0b\x32\x18.PAM.PAMRemoveController\"=\n\x10PAMModifyRequest\x12)\n\noperations\x18\x01 \x03(\x0b\x32\x15.PAM.PAMDataOperation\"\x98\x01\n\x10PAMDataOperation\x12,\n\roperationType\x18\x01 \x01(\x0e\x32\x15.PAM.PAMOperationType\x12\x30\n\rconfiguration\x18\x02 \x01(\x0b\x32\x19.PAM.PAMConfigurationData\x12$\n\x07\x65lement\x18\x03 \x01(\x0b\x32\x13.PAM.PAMElementData\"e\n\x14PAMConfigurationData\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\"E\n\x0ePAMElementData\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12\x11\n\tparentUid\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\"m\n\x19PAMElementOperationResult\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12+\n\x06result\x18\x02 \x01(\x0e\x32\x1b.PAM.PAMOperationResultType\x12\x0f\n\x07message\x18\x03 \x01(\t\"B\n\x0fPAMModifyResult\x12/\n\x07results\x18\x01 \x03(\x0b\x32\x1e.PAM.PAMElementOperationResult\"x\n\nPAMElement\x12\x12\n\nelementUid\x18\x01 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x03 \x01(\x03\x12\x14\n\x0clastModified\x18\x04 \x01(\x03\x12!\n\x08\x63hildren\x18\x05 \x03(\x0b\x32\x0f.PAM.PAMElement\"#\n\x14PAMGenericUidRequest\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\"%\n\x15PAMGenericUidsRequest\x12\x0c\n\x04uids\x18\x01 \x03(\x0c\"\xab\x01\n\x10PAMConfiguration\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x0e\n\x06nodeId\x18\x02 \x01(\x03\x12\x15\n\rcontrollerUid\x18\x03 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\x0c\x12\x0f\n\x07\x63reated\x18\x05 \x01(\x03\x12\x14\n\x0clastModified\x18\x06 \x01(\x03\x12!\n\x08\x63hildren\x18\x07 \x03(\x0b\x32\x0f.PAM.PAMElement\"B\n\x11PAMConfigurations\x12-\n\x0e\x63onfigurations\x18\x01 \x03(\x0b\x32\x15.PAM.PAMConfiguration\"\xab\x02\n\rPAMController\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x16\n\x0e\x63ontrollerName\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65viceToken\x18\x03 \x01(\t\x12\x12\n\ndeviceName\x18\x04 \x01(\t\x12\x0e\n\x06nodeId\x18\x05 \x01(\x03\x12\x0f\n\x07\x63reated\x18\x06 \x01(\x03\x12\x14\n\x0clastModified\x18\x07 \x01(\x03\x12\x16\n\x0e\x61pplicationUid\x18\x08 \x01(\x0c\x12\x30\n\rappClientType\x18\t \x01(\x0e\x32\x19.Enterprise.AppClientType\x12\x15\n\risInitialized\x18\n \x01(\x08\x12\x18\n\x10maxInstanceCount\x18\x0b \x01(\x05\x12\x10\n\x08lastSeen\x18\x0c \x01(\x03\"P\n\x1dPAMSetMaxInstanceCountRequest\x12\x15\n\rcontrollerUid\x18\x01 \x01(\x0c\x12\x18\n\x10maxInstanceCount\x18\x02 \x01(\x05\"%\n\x12\x43ontrollerResponse\x12\x0f\n\x07payload\x18\x01 \x01(\t\"M\n\x1aPAMConfigurationController\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x15\n\rcontrollerUid\x18\x02 \x01(\x0c\"\xa3\x01\n\x17\x43onfigurationAddRequest\x12\x18\n\x10\x63onfigurationUid\x18\x01 \x01(\x0c\x12\x11\n\trecordKey\x18\x02 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12(\n\x0brecordLinks\x18\x04 \x03(\x0b\x32\x13.Records.RecordLink\x12#\n\x05\x61udit\x18\x05 \x01(\x0b\x32\x14.Records.RecordAudit\"J\n\x10RelayAccessCreds\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x12\n\nserverTime\x18\x03 \x01(\x03\"\\\n\x10KturnAccessCreds\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\x0c\x12\x18\n\x10\x63\x65rt_fingerprint\x18\x03 \x01(\t\x12\x12\n\nexpires_at\x18\x04 \x01(\x04\"\x81\x02\n\x14PAMRecordingsRequest\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x10\n\x08maxCount\x18\x02 \x01(\x05\x12\x17\n\nrangeStart\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x15\n\x08rangeEnd\x18\x04 \x01(\x03H\x01\x88\x01\x01\x12$\n\x05types\x18\x05 \x03(\x0e\x32\x15.PAM.PAMRecordingType\x12)\n\x05risks\x18\x06 \x03(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x11\n\tprotocols\x18\x07 \x03(\t\x12\x14\n\x0c\x63loseReasons\x18\x08 \x03(\x05\x42\r\n\x0b_rangeStartB\x0b\n\t_rangeEnd\"\x89\x02\n\x1cPAMRecordingsForUsersRequest\x12\x11\n\tusernames\x18\x01 \x03(\t\x12\x10\n\x08maxCount\x18\x02 \x01(\x05\x12\x17\n\nrangeStart\x18\x03 \x01(\x03H\x00\x88\x01\x01\x12\x15\n\x08rangeEnd\x18\x04 \x01(\x03H\x01\x88\x01\x01\x12$\n\x05types\x18\x05 \x03(\x0e\x32\x15.PAM.PAMRecordingType\x12)\n\x05risks\x18\x06 \x03(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x11\n\tprotocols\x18\x07 \x03(\t\x12\x14\n\x0c\x63loseReasons\x18\x08 \x03(\x05\x42\r\n\x0b_rangeStartB\x0b\n\t_rangeEnd\"\xd4\x02\n\x0cPAMRecording\x12\x15\n\rconnectionUid\x18\x01 \x01(\x0c\x12,\n\rrecordingType\x18\x02 \x01(\x0e\x32\x15.PAM.PAMRecordingType\x12\x11\n\trecordUid\x18\x03 \x01(\x0c\x12\x10\n\x08userName\x18\x04 \x01(\t\x12\x11\n\tstartedOn\x18\x05 \x01(\x03\x12\x0e\n\x06length\x18\x06 \x01(\x05\x12\x10\n\x08\x66ileSize\x18\x07 \x01(\x03\x12\x11\n\tcreatedOn\x18\x08 \x01(\x03\x12\x10\n\x08protocol\x18\t \x01(\t\x12\x13\n\x0b\x63loseReason\x18\n \x01(\x05\x12\x19\n\x11recordingDuration\x18\x0b \x01(\x05\x12\x36\n\x12\x61iOverallRiskLevel\x18\x0c \x01(\x0e\x32\x1a.PAM.PAMRecordingRiskLevel\x12\x18\n\x10\x61iOverallSummary\x18\r \x01(\x0c\"O\n\x15PAMRecordingsResponse\x12%\n\nrecordings\x18\x01 \x03(\x0b\x32\x11.PAM.PAMRecording\x12\x0f\n\x07hasMore\x18\x02 \x01(\x08\"*\n\x07PAMData\x12\x0e\n\x06vertex\x18\x01 \x01(\x0c\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\x0c\"\x17\n\x07UidList\x12\x0c\n\x04uids\x18\x01 \x03(\x0c\"5\n\x0fPAMServiceNames\x12\x13\n\x0bresourceUid\x18\x01 \x01(\x0c\x12\r\n\x05names\x18\x02 \x01(\x0c\"\xb4\x03\n\x11PAMResourceConfig\x12\x11\n\trecordUid\x18\x01 \x01(\x0c\x12\x17\n\nnetworkUid\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\x15\n\x08\x61\x64minUid\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x12\x11\n\x04meta\x18\x04 \x01(\x0cH\x02\x88\x01\x01\x12\x1f\n\x12\x63onnectionSettings\x18\x05 \x01(\x0cH\x03\x88\x01\x01\x12\'\n\x0c\x63onnectUsers\x18\x06 \x01(\x0b\x32\x0c.PAM.UidListH\x04\x88\x01\x01\x12\x16\n\tdomainUid\x18\x07 \x01(\x0cH\x05\x88\x01\x01\x12\x18\n\x0bjitSettings\x18\x08 \x01(\x0cH\x06\x88\x01\x01\x12\x1d\n\x10keeperAiSettings\x18\t \x01(\x0cH\x07\x88\x01\x01\x12\x1b\n\x0eupdateServices\x18\n \x01(\x08H\x08\x88\x01\x01\x42\r\n\x0b_networkUidB\x0b\n\t_adminUidB\x07\n\x05_metaB\x15\n\x13_connectionSettingsB\x0f\n\r_connectUsersB\x0c\n\n_domainUidB\x0e\n\x0c_jitSettingsB\x13\n\x11_keeperAiSettingsB\x11\n\x0f_updateServices\"%\n\x16PAMUniversalSyncFolder\x12\x0b\n\x03uid\x18\x01 \x01(\x0c\" \n\x10GitHubRepository\x12\x0c\n\x04name\x18\x01 \x01(\x0c\"\xd6\x01\n\x0cGitHubConfig\x12$\n\x05scope\x18\x01 \x01(\x0e\x32\x10.PAM.GitHubScopeH\x00\x88\x01\x01\x12\r\n\x05owner\x18\x02 \x01(\x0c\x12\x46\n\x16organizationVisibility\x18\x03 \x01(\x0e\x32!.PAM.GitHubOrganizationVisibilityH\x01\x88\x01\x01\x12$\n\x05repos\x18\x04 \x03(\x0b\x32\x15.PAM.GitHubRepositoryB\x08\n\x06_scopeB\x19\n\x17_organizationVisibility\"\xaf\x02\n\x16PAMUniversalSyncConfig\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x14\n\x07\x65nabled\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1a\n\rdryRunEnabled\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12,\n\x07\x66olders\x18\x04 \x03(\x0b\x32\x1b.PAM.PAMUniversalSyncFolder\x12\x19\n\x0csyncIdentity\x18\x05 \x01(\x0cH\x02\x88\x01\x01\x12\x16\n\tvaultName\x18\x06 \x01(\x0cH\x03\x88\x01\x01\x12&\n\x06github\x18\x07 \x01(\x0b\x32\x11.PAM.GitHubConfigH\x04\x88\x01\x01\x42\n\n\x08_enabledB\x10\n\x0e_dryRunEnabledB\x0f\n\r_syncIdentityB\x0c\n\n_vaultNameB\t\n\x07_github\"7\n\x11NhiMetricsRequest\x12\x11\n\tstartTime\x18\x01 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x02 \x01(\x03\"\x9c\x02\n\x0ePamUsageByUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12!\n\x19recordRotationScheduledOk\x18\x02 \x01(\x05\x12\x1c\n\x14pamConnectionStarted\x18\x03 \x01(\x05\x12\x18\n\x10pamTunnelStarted\x18\x04 \x01(\x05\x12\x1b\n\x13\x64iscoveryJobStarted\x18\x05 \x01(\x05\x12 \n\x18recordRotationOnDemandOk\x18\x06 \x01(\x05\x12\"\n\x1apamSessionRecordingStarted\x18\x07 \x01(\x05\x12\x15\n\rpamRbiStarted\x18\x08 \x01(\x05\x12%\n\x1dpamSessionRbiRecordingStarted\x18\t \x01(\x05\"p\n\x0eNhiUsageByUser\x12\x0e\n\x06userId\x18\x01 \x01(\x05\x12\x11\n\trotations\x18\x02 \x01(\x05\x12\x0f\n\x07tunnels\x18\x03 \x01(\x05\x12\x13\n\x0b\x63onnections\x18\x04 \x01(\x05\x12\x15\n\rdiscoveryJobs\x18\x05 \x01(\x05\"\x84\x02\n\x12NhiMetricsResponse\x12\x14\n\x0c\x65nterpriseId\x18\x01 \x01(\x03\x12\x11\n\tstartTime\x18\x02 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x03 \x01(\x03\x12\x18\n\x10uniqueKsmDevices\x18\x04 \x01(\x05\x12\x18\n\x10pamGatewayOnline\x18\x05 \x01(\x05\x12/\n\x0epamUsageByUser\x18\x06 \x03(\x0b\x32\x13.PAM.PamUsageByUserB\x02\x18\x01\x12\x10\n\x08nhiCount\x18\x07 \x01(\x05\x12\x13\n\x0bksmNhiCount\x18\x08 \x01(\x05\x12(\n\x0busageByUser\x18\t \x03(\x0b\x32\x13.PAM.NhiUsageByUser\"D\n\x16NhiBulkMetricsResponse\x12*\n\tresponses\x18\x01 \x03(\x0b\x32\x17.PAM.NhiMetricsResponse\"^\n\x0bNhiUidEntry\x12\x0b\n\x03uid\x18\x01 \x01(\t\x12\"\n\x08\x63\x61tegory\x18\x02 \x01(\x0e\x32\x10.PAM.NhiCategory\x12\x0e\n\x06ksmNhi\x18\x03 \x01(\x08\x12\x0e\n\x06\x61ppUid\x18\x04 \x01(\t\"7\n\x11GetNhiUidsRequest\x12\x11\n\tstartTime\x18\x01 \x01(\x03\x12\x0f\n\x07\x65ndTime\x18\x02 \x01(\x03\"4\n\x12GetNhiUidsResponse\x12\x1e\n\x04uids\x18\x01 \x03(\x0b\x32\x10.PAM.NhiUidEntry\"6\n\x1dSetNhiKsmEffectiveDateRequest\x12\x15\n\reffectiveDate\x18\x01 \x01(\x03\"L\n\x1eGetNhiKsmEffectiveDateResponse\x12\x15\n\reffectiveDate\x18\x01 \x01(\x03\x12\x13\n\x0b\x64\x65\x66\x61ultDate\x18\x02 \x01(\x03\"I\n\x1fPAMUniversalSyncPreCheckRequest\x12\x12\n\nnetworkUid\x18\x01 \x01(\x0c\x12\x12\n\nfolderUids\x18\x02 \x03(\x0c\"C\n\x1ePAMUniversalSyncPreCheckResult\x12\x11\n\tfolderUid\x18\x01 \x01(\x0c\x12\x0e\n\x06isUsed\x18\x02 \x01(\x08\"X\n PAMUniversalSyncPreCheckResponse\x12\x34\n\x07results\x18\x01 \x03(\x0b\x32#.PAM.PAMUniversalSyncPreCheckResult*\x9e\x01\n\x14WebRtcConnectionType\x12\x0e\n\nCONNECTION\x10\x00\x12\n\n\x06TUNNEL\x10\x01\x12\x07\n\x03SSH\x10\x02\x12\x07\n\x03RDP\x10\x03\x12\x08\n\x04HTTP\x10\x04\x12\x07\n\x03VNC\x10\x05\x12\n\n\x06TELNET\x10\x06\x12\t\n\x05MYSQL\x10\x07\x12\x0e\n\nSQL_SERVER\x10\x08\x12\x0e\n\nPOSTGRESQL\x10\t\x12\x0e\n\nKUBERNETES\x10\n*@\n\x10PAMOperationType\x12\x07\n\x03\x41\x44\x44\x10\x00\x12\n\n\x06UPDATE\x10\x01\x12\x0b\n\x07REPLACE\x10\x02\x12\n\n\x06\x44\x45LETE\x10\x03*p\n\x16PAMOperationResultType\x12\x0f\n\x0bPOT_SUCCESS\x10\x00\x12\x15\n\x11POT_UNKNOWN_ERROR\x10\x01\x12\x16\n\x12POT_ALREADY_EXISTS\x10\x02\x12\x16\n\x12POT_DOES_NOT_EXIST\x10\x03*\xc9\x01\n\x15\x43ontrollerMessageType\x12\x0f\n\x0b\x43MT_GENERAL\x10\x00\x12\x0e\n\nCMT_ROTATE\x10\x01\x12\x11\n\rCMT_DISCOVERY\x10\x02\x12\x0f\n\x0b\x43MT_CONNECT\x10\x03\x12\x19\n\x15\x43MT_ANALYZE_RECORDING\x10\x04\x12!\n\x1d\x43MT_WORKFLOW_ACCESS_ELEVATION\x10\x05\x12\x0b\n\x07\x43MT_USS\x10\x06\x12\x0c\n\x08\x43MT_INFO\x10\x07\x12\x12\n\x0e\x43MT_AUTOMATION\x10\x08*V\n\x10PAMRecordingType\x12\x0f\n\x0bPRT_SESSION\x10\x00\x12\x12\n\x0ePRT_TYPESCRIPT\x10\x01\x12\x0c\n\x08PRT_TIME\x10\x02\x12\x0f\n\x0bPRT_SUMMARY\x10\x03*i\n\x15PAMRecordingRiskLevel\x12\x13\n\x0fPRR_UNSPECIFIED\x10\x00\x12\x0b\n\x07PRR_LOW\x10\x01\x12\x0e\n\nPRR_MEDIUM\x10\x02\x12\x0c\n\x08PRR_HIGH\x10\x03\x12\x10\n\x0cPRR_CRITICAL\x10\x04*/\n\x0bGitHubScope\x12\x0e\n\nREPOSITORY\x10\x00\x12\x10\n\x0cORGANIZATION\x10\x01*B\n\x1cGitHubOrganizationVisibility\x12\x07\n\x03\x41LL\x10\x00\x12\x0b\n\x07PRIVATE\x10\x01\x12\x0c\n\x08SELECTED\x10\x02*`\n\x0bNhiCategory\x12\x18\n\x14NHI_CATEGORY_UNKNOWN\x10\x00\x12\x0c\n\x08PAM_USER\x10\x01\x12\x10\n\x0cPAM_RESOURCE\x10\x02\x12\x0b\n\x07GATEWAY\x10\x03\x12\n\n\x06\x44\x45VICE\x10\x04\x42\x1f\n\x18\x63om.keepersecurity.protoB\x03PAMb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,20 +36,24 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\030com.keepersecurity.protoB\003PAM' _globals['_NHIMETRICSRESPONSE'].fields_by_name['pamUsageByUser']._loaded_options = None _globals['_NHIMETRICSRESPONSE'].fields_by_name['pamUsageByUser']._serialized_options = b'\030\001' - _globals['_WEBRTCCONNECTIONTYPE']._serialized_start=5340 - _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=5498 - _globals['_PAMOPERATIONTYPE']._serialized_start=5500 - _globals['_PAMOPERATIONTYPE']._serialized_end=5564 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=5566 - _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=5678 - _globals['_CONTROLLERMESSAGETYPE']._serialized_start=5681 - _globals['_CONTROLLERMESSAGETYPE']._serialized_end=5882 - _globals['_PAMRECORDINGTYPE']._serialized_start=5884 - _globals['_PAMRECORDINGTYPE']._serialized_end=5970 - _globals['_PAMRECORDINGRISKLEVEL']._serialized_start=5972 - _globals['_PAMRECORDINGRISKLEVEL']._serialized_end=6077 - _globals['_NHICATEGORY']._serialized_start=6079 - _globals['_NHICATEGORY']._serialized_end=6175 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_start=6267 + _globals['_WEBRTCCONNECTIONTYPE']._serialized_end=6425 + _globals['_PAMOPERATIONTYPE']._serialized_start=6427 + _globals['_PAMOPERATIONTYPE']._serialized_end=6491 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_start=6493 + _globals['_PAMOPERATIONRESULTTYPE']._serialized_end=6605 + _globals['_CONTROLLERMESSAGETYPE']._serialized_start=6608 + _globals['_CONTROLLERMESSAGETYPE']._serialized_end=6809 + _globals['_PAMRECORDINGTYPE']._serialized_start=6811 + _globals['_PAMRECORDINGTYPE']._serialized_end=6897 + _globals['_PAMRECORDINGRISKLEVEL']._serialized_start=6899 + _globals['_PAMRECORDINGRISKLEVEL']._serialized_end=7004 + _globals['_GITHUBSCOPE']._serialized_start=7006 + _globals['_GITHUBSCOPE']._serialized_end=7053 + _globals['_GITHUBORGANIZATIONVISIBILITY']._serialized_start=7055 + _globals['_GITHUBORGANIZATIONVISIBILITY']._serialized_end=7121 + _globals['_NHICATEGORY']._serialized_start=7123 + _globals['_NHICATEGORY']._serialized_end=7219 _globals['_PAMROTATIONSCHEDULE']._serialized_start=51 _globals['_PAMROTATIONSCHEDULE']._serialized_end=182 _globals['_PAMROTATIONSCHEDULESRESPONSE']._serialized_start=184 @@ -91,49 +95,67 @@ _globals['_PAMCONFIGURATIONS']._serialized_start=1883 _globals['_PAMCONFIGURATIONS']._serialized_end=1949 _globals['_PAMCONTROLLER']._serialized_start=1952 - _globals['_PAMCONTROLLER']._serialized_end=2207 - _globals['_PAMSETMAXINSTANCECOUNTREQUEST']._serialized_start=2209 - _globals['_PAMSETMAXINSTANCECOUNTREQUEST']._serialized_end=2289 - _globals['_CONTROLLERRESPONSE']._serialized_start=2291 - _globals['_CONTROLLERRESPONSE']._serialized_end=2328 - _globals['_PAMCONFIGURATIONCONTROLLER']._serialized_start=2330 - _globals['_PAMCONFIGURATIONCONTROLLER']._serialized_end=2407 - _globals['_CONFIGURATIONADDREQUEST']._serialized_start=2410 - _globals['_CONFIGURATIONADDREQUEST']._serialized_end=2573 - _globals['_RELAYACCESSCREDS']._serialized_start=2575 - _globals['_RELAYACCESSCREDS']._serialized_end=2649 - _globals['_PAMRECORDINGSREQUEST']._serialized_start=2652 - _globals['_PAMRECORDINGSREQUEST']._serialized_end=2909 - _globals['_PAMRECORDINGSFORUSERSREQUEST']._serialized_start=2912 - _globals['_PAMRECORDINGSFORUSERSREQUEST']._serialized_end=3177 - _globals['_PAMRECORDING']._serialized_start=3180 - _globals['_PAMRECORDING']._serialized_end=3520 - _globals['_PAMRECORDINGSRESPONSE']._serialized_start=3522 - _globals['_PAMRECORDINGSRESPONSE']._serialized_end=3601 - _globals['_PAMDATA']._serialized_start=3603 - _globals['_PAMDATA']._serialized_end=3645 - _globals['_UIDLIST']._serialized_start=3647 - _globals['_UIDLIST']._serialized_end=3670 - _globals['_PAMRESOURCECONFIG']._serialized_start=3673 - _globals['_PAMRESOURCECONFIG']._serialized_end=4061 - _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_start=4063 - _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_end=4100 - _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_start=4103 - _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_end=4355 - _globals['_NHIMETRICSREQUEST']._serialized_start=4357 - _globals['_NHIMETRICSREQUEST']._serialized_end=4412 - _globals['_PAMUSAGEBYUSER']._serialized_start=4415 - _globals['_PAMUSAGEBYUSER']._serialized_end=4699 - _globals['_NHIUSAGEBYUSER']._serialized_start=4701 - _globals['_NHIUSAGEBYUSER']._serialized_end=4813 - _globals['_NHIMETRICSRESPONSE']._serialized_start=4816 - _globals['_NHIMETRICSRESPONSE']._serialized_end=5076 - _globals['_NHIBULKMETRICSRESPONSE']._serialized_start=5078 - _globals['_NHIBULKMETRICSRESPONSE']._serialized_end=5146 - _globals['_NHIUIDENTRY']._serialized_start=5148 - _globals['_NHIUIDENTRY']._serialized_end=5226 - _globals['_GETNHIUIDSREQUEST']._serialized_start=5228 - _globals['_GETNHIUIDSREQUEST']._serialized_end=5283 - _globals['_GETNHIUIDSRESPONSE']._serialized_start=5285 - _globals['_GETNHIUIDSRESPONSE']._serialized_end=5337 + _globals['_PAMCONTROLLER']._serialized_end=2251 + _globals['_PAMSETMAXINSTANCECOUNTREQUEST']._serialized_start=2253 + _globals['_PAMSETMAXINSTANCECOUNTREQUEST']._serialized_end=2333 + _globals['_CONTROLLERRESPONSE']._serialized_start=2335 + _globals['_CONTROLLERRESPONSE']._serialized_end=2372 + _globals['_PAMCONFIGURATIONCONTROLLER']._serialized_start=2374 + _globals['_PAMCONFIGURATIONCONTROLLER']._serialized_end=2451 + _globals['_CONFIGURATIONADDREQUEST']._serialized_start=2454 + _globals['_CONFIGURATIONADDREQUEST']._serialized_end=2617 + _globals['_RELAYACCESSCREDS']._serialized_start=2619 + _globals['_RELAYACCESSCREDS']._serialized_end=2693 + _globals['_KTURNACCESSCREDS']._serialized_start=2695 + _globals['_KTURNACCESSCREDS']._serialized_end=2787 + _globals['_PAMRECORDINGSREQUEST']._serialized_start=2790 + _globals['_PAMRECORDINGSREQUEST']._serialized_end=3047 + _globals['_PAMRECORDINGSFORUSERSREQUEST']._serialized_start=3050 + _globals['_PAMRECORDINGSFORUSERSREQUEST']._serialized_end=3315 + _globals['_PAMRECORDING']._serialized_start=3318 + _globals['_PAMRECORDING']._serialized_end=3658 + _globals['_PAMRECORDINGSRESPONSE']._serialized_start=3660 + _globals['_PAMRECORDINGSRESPONSE']._serialized_end=3739 + _globals['_PAMDATA']._serialized_start=3741 + _globals['_PAMDATA']._serialized_end=3783 + _globals['_UIDLIST']._serialized_start=3785 + _globals['_UIDLIST']._serialized_end=3808 + _globals['_PAMSERVICENAMES']._serialized_start=3810 + _globals['_PAMSERVICENAMES']._serialized_end=3863 + _globals['_PAMRESOURCECONFIG']._serialized_start=3866 + _globals['_PAMRESOURCECONFIG']._serialized_end=4302 + _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_start=4304 + _globals['_PAMUNIVERSALSYNCFOLDER']._serialized_end=4341 + _globals['_GITHUBREPOSITORY']._serialized_start=4343 + _globals['_GITHUBREPOSITORY']._serialized_end=4375 + _globals['_GITHUBCONFIG']._serialized_start=4378 + _globals['_GITHUBCONFIG']._serialized_end=4592 + _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_start=4595 + _globals['_PAMUNIVERSALSYNCCONFIG']._serialized_end=4898 + _globals['_NHIMETRICSREQUEST']._serialized_start=4900 + _globals['_NHIMETRICSREQUEST']._serialized_end=4955 + _globals['_PAMUSAGEBYUSER']._serialized_start=4958 + _globals['_PAMUSAGEBYUSER']._serialized_end=5242 + _globals['_NHIUSAGEBYUSER']._serialized_start=5244 + _globals['_NHIUSAGEBYUSER']._serialized_end=5356 + _globals['_NHIMETRICSRESPONSE']._serialized_start=5359 + _globals['_NHIMETRICSRESPONSE']._serialized_end=5619 + _globals['_NHIBULKMETRICSRESPONSE']._serialized_start=5621 + _globals['_NHIBULKMETRICSRESPONSE']._serialized_end=5689 + _globals['_NHIUIDENTRY']._serialized_start=5691 + _globals['_NHIUIDENTRY']._serialized_end=5785 + _globals['_GETNHIUIDSREQUEST']._serialized_start=5787 + _globals['_GETNHIUIDSREQUEST']._serialized_end=5842 + _globals['_GETNHIUIDSRESPONSE']._serialized_start=5844 + _globals['_GETNHIUIDSRESPONSE']._serialized_end=5896 + _globals['_SETNHIKSMEFFECTIVEDATEREQUEST']._serialized_start=5898 + _globals['_SETNHIKSMEFFECTIVEDATEREQUEST']._serialized_end=5952 + _globals['_GETNHIKSMEFFECTIVEDATERESPONSE']._serialized_start=5954 + _globals['_GETNHIKSMEFFECTIVEDATERESPONSE']._serialized_end=6030 + _globals['_PAMUNIVERSALSYNCPRECHECKREQUEST']._serialized_start=6032 + _globals['_PAMUNIVERSALSYNCPRECHECKREQUEST']._serialized_end=6105 + _globals['_PAMUNIVERSALSYNCPRECHECKRESULT']._serialized_start=6107 + _globals['_PAMUNIVERSALSYNCPRECHECKRESULT']._serialized_end=6174 + _globals['_PAMUNIVERSALSYNCPRECHECKRESPONSE']._serialized_start=6176 + _globals['_PAMUNIVERSALSYNCPRECHECKRESPONSE']._serialized_end=6264 # @@protoc_insertion_point(module_scope) diff --git a/keepercommander/proto/pam_pb2.pyi b/keepercommander/proto/pam_pb2.pyi index b5a56975a..b83fdc735 100644 --- a/keepercommander/proto/pam_pb2.pyi +++ b/keepercommander/proto/pam_pb2.pyi @@ -4,7 +4,8 @@ from google.protobuf.internal import containers as _containers from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message -from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union +from collections.abc import Iterable as _Iterable, Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -63,6 +64,17 @@ class PAMRecordingRiskLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): PRR_HIGH: _ClassVar[PAMRecordingRiskLevel] PRR_CRITICAL: _ClassVar[PAMRecordingRiskLevel] +class GitHubScope(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + REPOSITORY: _ClassVar[GitHubScope] + ORGANIZATION: _ClassVar[GitHubScope] + +class GitHubOrganizationVisibility(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): + __slots__ = () + ALL: _ClassVar[GitHubOrganizationVisibility] + PRIVATE: _ClassVar[GitHubOrganizationVisibility] + SELECTED: _ClassVar[GitHubOrganizationVisibility] + class NhiCategory(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): __slots__ = () NHI_CATEGORY_UNKNOWN: _ClassVar[NhiCategory] @@ -107,6 +119,11 @@ PRR_LOW: PAMRecordingRiskLevel PRR_MEDIUM: PAMRecordingRiskLevel PRR_HIGH: PAMRecordingRiskLevel PRR_CRITICAL: PAMRecordingRiskLevel +REPOSITORY: GitHubScope +ORGANIZATION: GitHubScope +ALL: GitHubOrganizationVisibility +PRIVATE: GitHubOrganizationVisibility +SELECTED: GitHubOrganizationVisibility NHI_CATEGORY_UNKNOWN: NhiCategory PAM_USER: NhiCategory PAM_RESOURCE: NhiCategory @@ -125,7 +142,7 @@ class PAMRotationSchedule(_message.Message): controllerUid: bytes scheduleData: str noSchedule: bool - def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., scheduleData: _Optional[str] = ..., noSchedule: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., configurationUid: _Optional[bytes] = ..., controllerUid: _Optional[bytes] = ..., scheduleData: _Optional[str] = ..., noSchedule: _Optional[bool] = ...) -> None: ... class PAMRotationSchedulesResponse(_message.Message): __slots__ = ("schedules",) @@ -304,7 +321,7 @@ class PAMConfigurations(_message.Message): def __init__(self, configurations: _Optional[_Iterable[_Union[PAMConfiguration, _Mapping]]] = ...) -> None: ... class PAMController(_message.Message): - __slots__ = ("controllerUid", "controllerName", "deviceToken", "deviceName", "nodeId", "created", "lastModified", "applicationUid", "appClientType", "isInitialized") + __slots__ = ("controllerUid", "controllerName", "deviceToken", "deviceName", "nodeId", "created", "lastModified", "applicationUid", "appClientType", "isInitialized", "maxInstanceCount", "lastSeen") CONTROLLERUID_FIELD_NUMBER: _ClassVar[int] CONTROLLERNAME_FIELD_NUMBER: _ClassVar[int] DEVICETOKEN_FIELD_NUMBER: _ClassVar[int] @@ -315,6 +332,8 @@ class PAMController(_message.Message): APPLICATIONUID_FIELD_NUMBER: _ClassVar[int] APPCLIENTTYPE_FIELD_NUMBER: _ClassVar[int] ISINITIALIZED_FIELD_NUMBER: _ClassVar[int] + MAXINSTANCECOUNT_FIELD_NUMBER: _ClassVar[int] + LASTSEEN_FIELD_NUMBER: _ClassVar[int] controllerUid: bytes controllerName: str deviceToken: str @@ -325,7 +344,9 @@ class PAMController(_message.Message): applicationUid: bytes appClientType: _enterprise_pb2.AppClientType isInitialized: bool - def __init__(self, controllerUid: _Optional[bytes] = ..., controllerName: _Optional[str] = ..., deviceToken: _Optional[str] = ..., deviceName: _Optional[str] = ..., nodeId: _Optional[int] = ..., created: _Optional[int] = ..., lastModified: _Optional[int] = ..., applicationUid: _Optional[bytes] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., isInitialized: bool = ...) -> None: ... + maxInstanceCount: int + lastSeen: int + def __init__(self, controllerUid: _Optional[bytes] = ..., controllerName: _Optional[str] = ..., deviceToken: _Optional[str] = ..., deviceName: _Optional[str] = ..., nodeId: _Optional[int] = ..., created: _Optional[int] = ..., lastModified: _Optional[int] = ..., applicationUid: _Optional[bytes] = ..., appClientType: _Optional[_Union[_enterprise_pb2.AppClientType, str]] = ..., isInitialized: _Optional[bool] = ..., maxInstanceCount: _Optional[int] = ..., lastSeen: _Optional[int] = ...) -> None: ... class PAMSetMaxInstanceCountRequest(_message.Message): __slots__ = ("controllerUid", "maxInstanceCount") @@ -373,6 +394,18 @@ class RelayAccessCreds(_message.Message): serverTime: int def __init__(self, username: _Optional[str] = ..., password: _Optional[str] = ..., serverTime: _Optional[int] = ...) -> None: ... +class KturnAccessCreds(_message.Message): + __slots__ = ("url", "token", "cert_fingerprint", "expires_at") + URL_FIELD_NUMBER: _ClassVar[int] + TOKEN_FIELD_NUMBER: _ClassVar[int] + CERT_FINGERPRINT_FIELD_NUMBER: _ClassVar[int] + EXPIRES_AT_FIELD_NUMBER: _ClassVar[int] + url: str + token: bytes + cert_fingerprint: str + expires_at: int + def __init__(self, url: _Optional[str] = ..., token: _Optional[bytes] = ..., cert_fingerprint: _Optional[str] = ..., expires_at: _Optional[int] = ...) -> None: ... + class PAMRecordingsRequest(_message.Message): __slots__ = ("recordUid", "maxCount", "rangeStart", "rangeEnd", "types", "risks", "protocols", "closeReasons") RECORDUID_FIELD_NUMBER: _ClassVar[int] @@ -393,6 +426,26 @@ class PAMRecordingsRequest(_message.Message): closeReasons: _containers.RepeatedScalarFieldContainer[int] def __init__(self, recordUid: _Optional[bytes] = ..., maxCount: _Optional[int] = ..., rangeStart: _Optional[int] = ..., rangeEnd: _Optional[int] = ..., types: _Optional[_Iterable[_Union[PAMRecordingType, str]]] = ..., risks: _Optional[_Iterable[_Union[PAMRecordingRiskLevel, str]]] = ..., protocols: _Optional[_Iterable[str]] = ..., closeReasons: _Optional[_Iterable[int]] = ...) -> None: ... +class PAMRecordingsForUsersRequest(_message.Message): + __slots__ = ("usernames", "maxCount", "rangeStart", "rangeEnd", "types", "risks", "protocols", "closeReasons") + USERNAMES_FIELD_NUMBER: _ClassVar[int] + MAXCOUNT_FIELD_NUMBER: _ClassVar[int] + RANGESTART_FIELD_NUMBER: _ClassVar[int] + RANGEEND_FIELD_NUMBER: _ClassVar[int] + TYPES_FIELD_NUMBER: _ClassVar[int] + RISKS_FIELD_NUMBER: _ClassVar[int] + PROTOCOLS_FIELD_NUMBER: _ClassVar[int] + CLOSEREASONS_FIELD_NUMBER: _ClassVar[int] + usernames: _containers.RepeatedScalarFieldContainer[str] + maxCount: int + rangeStart: int + rangeEnd: int + types: _containers.RepeatedScalarFieldContainer[PAMRecordingType] + risks: _containers.RepeatedScalarFieldContainer[PAMRecordingRiskLevel] + protocols: _containers.RepeatedScalarFieldContainer[str] + closeReasons: _containers.RepeatedScalarFieldContainer[int] + def __init__(self, usernames: _Optional[_Iterable[str]] = ..., maxCount: _Optional[int] = ..., rangeStart: _Optional[int] = ..., rangeEnd: _Optional[int] = ..., types: _Optional[_Iterable[_Union[PAMRecordingType, str]]] = ..., risks: _Optional[_Iterable[_Union[PAMRecordingRiskLevel, str]]] = ..., protocols: _Optional[_Iterable[str]] = ..., closeReasons: _Optional[_Iterable[int]] = ...) -> None: ... + class PAMRecording(_message.Message): __slots__ = ("connectionUid", "recordingType", "recordUid", "userName", "startedOn", "length", "fileSize", "createdOn", "protocol", "closeReason", "recordingDuration", "aiOverallRiskLevel", "aiOverallSummary") CONNECTIONUID_FIELD_NUMBER: _ClassVar[int] @@ -429,7 +482,7 @@ class PAMRecordingsResponse(_message.Message): HASMORE_FIELD_NUMBER: _ClassVar[int] recordings: _containers.RepeatedCompositeFieldContainer[PAMRecording] hasMore: bool - def __init__(self, recordings: _Optional[_Iterable[_Union[PAMRecording, _Mapping]]] = ..., hasMore: bool = ...) -> None: ... + def __init__(self, recordings: _Optional[_Iterable[_Union[PAMRecording, _Mapping]]] = ..., hasMore: _Optional[bool] = ...) -> None: ... class PAMData(_message.Message): __slots__ = ("vertex", "content") @@ -445,6 +498,14 @@ class UidList(_message.Message): uids: _containers.RepeatedScalarFieldContainer[bytes] def __init__(self, uids: _Optional[_Iterable[bytes]] = ...) -> None: ... +class PAMServiceNames(_message.Message): + __slots__ = ("resourceUid", "names") + RESOURCEUID_FIELD_NUMBER: _ClassVar[int] + NAMES_FIELD_NUMBER: _ClassVar[int] + resourceUid: bytes + names: bytes + def __init__(self, resourceUid: _Optional[bytes] = ..., names: _Optional[bytes] = ...) -> None: ... + class PAMResourceConfig(_message.Message): __slots__ = ("recordUid", "networkUid", "adminUid", "meta", "connectionSettings", "connectUsers", "domainUid", "jitSettings", "keeperAiSettings", "updateServices") RECORDUID_FIELD_NUMBER: _ClassVar[int] @@ -467,7 +528,7 @@ class PAMResourceConfig(_message.Message): jitSettings: bytes keeperAiSettings: bytes updateServices: bool - def __init__(self, recordUid: _Optional[bytes] = ..., networkUid: _Optional[bytes] = ..., adminUid: _Optional[bytes] = ..., meta: _Optional[bytes] = ..., connectionSettings: _Optional[bytes] = ..., connectUsers: _Optional[_Union[UidList, _Mapping]] = ..., domainUid: _Optional[bytes] = ..., jitSettings: _Optional[bytes] = ..., keeperAiSettings: _Optional[bytes] = ..., updateServices: bool = ...) -> None: ... + def __init__(self, recordUid: _Optional[bytes] = ..., networkUid: _Optional[bytes] = ..., adminUid: _Optional[bytes] = ..., meta: _Optional[bytes] = ..., connectionSettings: _Optional[bytes] = ..., connectUsers: _Optional[_Union[UidList, _Mapping]] = ..., domainUid: _Optional[bytes] = ..., jitSettings: _Optional[bytes] = ..., keeperAiSettings: _Optional[bytes] = ..., updateServices: _Optional[bool] = ...) -> None: ... class PAMUniversalSyncFolder(_message.Message): __slots__ = ("uid",) @@ -475,21 +536,41 @@ class PAMUniversalSyncFolder(_message.Message): uid: bytes def __init__(self, uid: _Optional[bytes] = ...) -> None: ... +class GitHubRepository(_message.Message): + __slots__ = ("name",) + NAME_FIELD_NUMBER: _ClassVar[int] + name: bytes + def __init__(self, name: _Optional[bytes] = ...) -> None: ... + +class GitHubConfig(_message.Message): + __slots__ = ("scope", "owner", "organizationVisibility", "repos") + SCOPE_FIELD_NUMBER: _ClassVar[int] + OWNER_FIELD_NUMBER: _ClassVar[int] + ORGANIZATIONVISIBILITY_FIELD_NUMBER: _ClassVar[int] + REPOS_FIELD_NUMBER: _ClassVar[int] + scope: GitHubScope + owner: bytes + organizationVisibility: GitHubOrganizationVisibility + repos: _containers.RepeatedCompositeFieldContainer[GitHubRepository] + def __init__(self, scope: _Optional[_Union[GitHubScope, str]] = ..., owner: _Optional[bytes] = ..., organizationVisibility: _Optional[_Union[GitHubOrganizationVisibility, str]] = ..., repos: _Optional[_Iterable[_Union[GitHubRepository, _Mapping]]] = ...) -> None: ... + class PAMUniversalSyncConfig(_message.Message): - __slots__ = ("networkUid", "enabled", "dryRunEnabled", "folders", "syncIdentity", "vaultName") + __slots__ = ("networkUid", "enabled", "dryRunEnabled", "folders", "syncIdentity", "vaultName", "github") NETWORKUID_FIELD_NUMBER: _ClassVar[int] ENABLED_FIELD_NUMBER: _ClassVar[int] DRYRUNENABLED_FIELD_NUMBER: _ClassVar[int] FOLDERS_FIELD_NUMBER: _ClassVar[int] SYNCIDENTITY_FIELD_NUMBER: _ClassVar[int] VAULTNAME_FIELD_NUMBER: _ClassVar[int] + GITHUB_FIELD_NUMBER: _ClassVar[int] networkUid: bytes enabled: bool dryRunEnabled: bool folders: _containers.RepeatedCompositeFieldContainer[PAMUniversalSyncFolder] syncIdentity: bytes vaultName: bytes - def __init__(self, networkUid: _Optional[bytes] = ..., enabled: bool = ..., dryRunEnabled: bool = ..., folders: _Optional[_Iterable[_Union[PAMUniversalSyncFolder, _Mapping]]] = ..., syncIdentity: _Optional[bytes] = ..., vaultName: _Optional[bytes] = ...) -> None: ... + github: GitHubConfig + def __init__(self, networkUid: _Optional[bytes] = ..., enabled: _Optional[bool] = ..., dryRunEnabled: _Optional[bool] = ..., folders: _Optional[_Iterable[_Union[PAMUniversalSyncFolder, _Mapping]]] = ..., syncIdentity: _Optional[bytes] = ..., vaultName: _Optional[bytes] = ..., github: _Optional[_Union[GitHubConfig, _Mapping]] = ...) -> None: ... class NhiMetricsRequest(_message.Message): __slots__ = ("startTime", "endTime") @@ -564,14 +645,16 @@ class NhiBulkMetricsResponse(_message.Message): def __init__(self, responses: _Optional[_Iterable[_Union[NhiMetricsResponse, _Mapping]]] = ...) -> None: ... class NhiUidEntry(_message.Message): - __slots__ = ("uid", "category", "ksmNhi") + __slots__ = ("uid", "category", "ksmNhi", "appUid") UID_FIELD_NUMBER: _ClassVar[int] CATEGORY_FIELD_NUMBER: _ClassVar[int] KSMNHI_FIELD_NUMBER: _ClassVar[int] + APPUID_FIELD_NUMBER: _ClassVar[int] uid: str category: NhiCategory ksmNhi: bool - def __init__(self, uid: _Optional[str] = ..., category: _Optional[_Union[NhiCategory, str]] = ..., ksmNhi: bool = ...) -> None: ... + appUid: str + def __init__(self, uid: _Optional[str] = ..., category: _Optional[_Union[NhiCategory, str]] = ..., ksmNhi: _Optional[bool] = ..., appUid: _Optional[str] = ...) -> None: ... class GetNhiUidsRequest(_message.Message): __slots__ = ("startTime", "endTime") @@ -587,6 +670,20 @@ class GetNhiUidsResponse(_message.Message): uids: _containers.RepeatedCompositeFieldContainer[NhiUidEntry] def __init__(self, uids: _Optional[_Iterable[_Union[NhiUidEntry, _Mapping]]] = ...) -> None: ... +class SetNhiKsmEffectiveDateRequest(_message.Message): + __slots__ = ("effectiveDate",) + EFFECTIVEDATE_FIELD_NUMBER: _ClassVar[int] + effectiveDate: int + def __init__(self, effectiveDate: _Optional[int] = ...) -> None: ... + +class GetNhiKsmEffectiveDateResponse(_message.Message): + __slots__ = ("effectiveDate", "defaultDate") + EFFECTIVEDATE_FIELD_NUMBER: _ClassVar[int] + DEFAULTDATE_FIELD_NUMBER: _ClassVar[int] + effectiveDate: int + defaultDate: int + def __init__(self, effectiveDate: _Optional[int] = ..., defaultDate: _Optional[int] = ...) -> None: ... + class PAMUniversalSyncPreCheckRequest(_message.Message): __slots__ = ("networkUid", "folderUids") NETWORKUID_FIELD_NUMBER: _ClassVar[int] @@ -601,7 +698,7 @@ class PAMUniversalSyncPreCheckResult(_message.Message): ISUSED_FIELD_NUMBER: _ClassVar[int] folderUid: bytes isUsed: bool - def __init__(self, folderUid: _Optional[bytes] = ..., isUsed: bool = ...) -> None: ... + def __init__(self, folderUid: _Optional[bytes] = ..., isUsed: _Optional[bool] = ...) -> None: ... class PAMUniversalSyncPreCheckResponse(_message.Message): __slots__ = ("results",) diff --git a/unit-tests/pam/test_pam_nsf_config.py b/unit-tests/pam/test_pam_nsf_config.py index aa75e930c..30086837a 100644 --- a/unit-tests/pam/test_pam_nsf_config.py +++ b/unit-tests/pam/test_pam_nsf_config.py @@ -345,6 +345,98 @@ def parse_properties(_, record, **kwargs): params, 'config_uid', 'legacy_folder', command='pam-config-edit') +class TestPamConfigRequiredFieldValidation(unittest.TestCase): + """Regression tests: an empty required field (e.g. a GitHub config with no PAT) must abort + the whole operation instead of just logging a warning after the record was already saved.""" + + @staticmethod + def _make_missing_pat_field(): + field = vault.TypedField.new_field('secret', None, 'pamGitHubPersonalAccessToken') + field.required = True + return field + + @mock.patch('keepercommander.commands.discoveryrotation.create_pam_configuration_in_folder') + @mock.patch('keepercommander.commands.discoveryrotation.RecordEditMixin.get_record_type_fields', + return_value=[]) + def test_new_raises_and_does_not_create_record_when_required_field_empty( + self, mock_record_fields, mock_create_config): + params = _make_params() + folder = SharedFolderNode() + folder.uid = 'legacy_folder' + params.folder_cache[folder.uid] = folder + command = PAMConfigurationNewCommand() + + def parse_properties(_, record, **kwargs): + record.fields.append(vault.TypedField.new_field( + 'pamResources', {'folderUid': 'legacy_folder'})) + record.fields.append(self._make_missing_pat_field()) + + with mock.patch.object(command, 'parse_properties', side_effect=parse_properties): + with self.assertRaises(CommandError) as ctx: + command.execute(params, config_type='github', title='GitHub Config') + + self.assertIn('pamGitHubPersonalAccessToken', str(ctx.exception)) + mock_create_config.assert_not_called() + + @mock.patch('keepercommander.commands.discoveryrotation.update_pam_record') + @mock.patch('keepercommander.commands.discoveryrotation.RecordEditMixin.get_record_type_fields', + return_value=[]) + @mock.patch('keepercommander.vault.KeeperRecord.load') + def test_edit_raises_and_does_not_update_record_when_required_field_empty( + self, mock_load, mock_record_fields, mock_update_record): + params = _make_params() + folder = SharedFolderNode() + folder.uid = 'legacy_folder' + params.folder_cache[folder.uid] = folder + params.record_cache = {'config_uid': {}} + command = PAMConfigurationEditCommand() + + configuration = vault.TypedRecord(version=6) + configuration.record_uid = 'config_uid' + configuration.type_name = 'pamGitHubConfiguration' + configuration.fields.append(vault.TypedField.new_field( + 'pamResources', {'folderUid': 'legacy_folder'})) + mock_load.return_value = configuration + + def parse_properties(_, record, **kwargs): + record.fields.append(self._make_missing_pat_field()) + + with mock.patch.object(command, 'parse_properties', side_effect=parse_properties): + with self.assertRaises(CommandError) as ctx: + command.execute(params, uid='config_uid') + + self.assertIn('pamGitHubPersonalAccessToken', str(ctx.exception)) + mock_update_record.assert_not_called() + + +class TestPamConfigGitHubFieldMapping(unittest.TestCase): + """Regression test: --personal-access-token must land on the schema field the backend + actually marks required ("(secret).pamGitHubPersonalAccessToken"), not on a differently + labeled field, or the value is silently dropped and "pam config new" always aborts.""" + + def test_personal_access_token_populates_the_required_schema_field(self): + params = _make_params() + record = vault.TypedRecord(version=6) + record.type_name = 'pamGitHubConfiguration' + record.fields.append(vault.TypedField.new_field( + 'pamResources', {'folderUid': 'legacy_folder'})) + required_field = vault.TypedField.new_field('secret', None, 'pamGitHubPersonalAccessToken') + required_field.required = True + record.fields.append(required_field) + + command = PAMConfigurationNewCommand() + command.parse_properties( + params, record, github_id='github-test-acct', personal_access_token='github_pat_example') + + field = record.get_typed_field('secret', 'pamGitHubPersonalAccessToken') + self.assertIsNotNone(field) + self.assertEqual(field.get_default_value(str), 'github_pat_example') + # Regression guard: previously the mismatched label ("personalAccessToken" instead of + # "pamGitHubPersonalAccessToken") caused the value to be misfiled into a new custom field, + # leaving the actual required field empty. + self.assertIsNone(record.get_typed_field('secret', 'personalAccessToken')) + + class TestPamConfigRemoveNsf(unittest.TestCase): @mock.patch('keepercommander.vault.KeeperRecord.load') @@ -389,6 +481,21 @@ def test_resolve_pam_config_uid_finds_nsf_record_by_title(self, mock_load): params, 'PAM NSF Test Configuration') self.assertEqual(uid, 'config_uid') + @mock.patch('keepercommander.vault.KeeperRecord.load') + def test_resolve_pam_config_uid_finds_github_config_by_uid(self, mock_load): + # Regression test: pamGitHubConfiguration must be resolvable like every other + # PAM configuration type, or "pam config edit"/"pam config remove" can never find it. + params = _make_params() + params.record_cache = {'config_uid': {}} + + configuration = vault.TypedRecord(version=6) + configuration.record_uid = 'config_uid' + configuration.type_name = 'pamGitHubConfiguration' + mock_load.return_value = configuration + + uid = PAMConfigurationRemoveCommand._resolve_pam_config_uid(params, 'config_uid') + self.assertEqual(uid, 'config_uid') + @mock.patch('keepercommander.commands.pam.config_helper.RecordRemoveCommand') @mock.patch('keepercommander.commands.nested_share_folder.helpers.is_nested_share_record', return_value=False) diff --git a/unit-tests/pam/test_uss_github_config.py b/unit-tests/pam/test_uss_github_config.py new file mode 100644 index 000000000..cb05596f5 --- /dev/null +++ b/unit-tests/pam/test_uss_github_config.py @@ -0,0 +1,239 @@ +""" +Unit tests for the Universal Secrets Sync (USS) GitHub configuration fields on +`pam universal-sync-config add / edit / list`. + +Covers the nested `GitHubConfig` shape documented in krouter's +`docs/User API/ConfigureUniversalSync.md`: GitHub's scope/owner/organizationVisibility/repos +live under a single `github` message field on `PAMUniversalSyncConfig`, not as flat fields. +""" +import json +import unittest +from unittest import mock + +from keepercommander import crypto, utils, vault +from keepercommander.commands.universalsecretsync import ( + PAMUniversalSyncConfigAddCommand, + PAMUniversalSyncConfigEditCommand, + PAMUniversalSyncConfigListCommand, +) +from keepercommander.proto import pam_pb2 + +NETWORK_UID = 'AAAAAAAAAAAAAAAAAAAAAA' # roundtrip-safe base64url, 22 chars +RECORD_KEY = bytes(range(16)) +FAKE_TOKENS = (b'enc-session-token', b'enc-transmission-key', b'transmission-key') + + +def _make_network_record(record_type='pamGitHubConfiguration'): + network = vault.TypedRecord() + network.record_uid = NETWORK_UID + network.record_key = RECORD_KEY + network.title = 'GitHub Network' + network.type_name = record_type + return network + + +class FakeEdge: + def __init__(self, path, head_uid, content=None): + self.path = path + self.head_uid = head_uid + self._content = content + + @property + def content_as_dict(self): + return self._content + + +class FakeVertex: + def __init__(self, uid, edges=None, child_vertices=None): + self.uid = uid + self.edges = edges or [] + self._child_vertices = child_vertices or [] + + def has_vertices(self, *_args, **_kwargs): + return self._child_vertices + + +def _patch_dag(existing_config=None): + """Build mock DAG/Connection classes matching the lazy `from ..keeper_dag import DAG` + imports inside universalsecretsync.py. Returns (dag_class, connection_class, root).""" + root = FakeVertex(uid=NETWORK_UID) + if existing_config is not None: + root.edges = [FakeEdge('universal_sync', head_uid=NETWORK_UID, content=existing_config)] + + dag_instance = mock.MagicMock() + dag_instance.get_root = root + dag_instance.load.return_value = None + + dag_class = mock.MagicMock(return_value=dag_instance) + connection_class = mock.MagicMock() + return dag_class, connection_class, root + + +class TestPAMUniversalSyncConfigAddCommand(unittest.TestCase): + def setUp(self): + self.params = mock.MagicMock() + self.network = _make_network_record() + mock.patch('keepercommander.commands.universalsecretsync.get_keeper_tokens', + return_value=FAKE_TOKENS).start() + mock.patch('keepercommander.vault.KeeperRecord.load', return_value=self.network).start() + self.mock_router = mock.patch( + 'keepercommander.commands.universalsecretsync.router_helper.router_configure_universal_sync').start() + self.addCleanup(mock.patch.stopall) + + def _execute(self, **kwargs): + kwargs.setdefault('network', NETWORK_UID) + kwargs.setdefault('enabled', 'true') + kwargs.setdefault('dry_run', 'false') + PAMUniversalSyncConfigAddCommand().execute(self.params, **kwargs) + self.assertEqual(self.mock_router.call_count, 1) + return self.mock_router.call_args[0][1] + + def test_writes_github_config_under_nested_field(self): + rq = self._execute(scope='organization', owner='my-org', org_visibility='selected', + repo=['repo-a', 'repo-b']) + + self.assertTrue(rq.HasField('github')) + self.assertEqual(rq.github.scope, pam_pb2.ORGANIZATION) + self.assertEqual(rq.github.organizationVisibility, pam_pb2.SELECTED) + self.assertEqual(crypto.decrypt_aes_v2(rq.github.owner, RECORD_KEY), b'my-org') + + self.assertEqual(len(rq.github.repos), 2) + decrypted_repos = [crypto.decrypt_aes_v2(r.name, RECORD_KEY).decode('utf-8') for r in rq.github.repos] + self.assertEqual(decrypted_repos, ['repo-a', 'repo-b']) + + def test_repository_scope_maps_correctly(self): + rq = self._execute(scope='repository', owner='my-user') + self.assertEqual(rq.github.scope, pam_pb2.REPOSITORY) + + def test_no_github_flags_leaves_github_field_unset(self): + rq = self._execute() + self.assertFalse(rq.HasField('github')) + + def test_flat_fields_no_longer_exist_on_the_message(self): + rq = pam_pb2.PAMUniversalSyncConfig() + for flat_field in ('scope', 'owner', 'organizationVisibility', 'repos'): + with self.assertRaises(AttributeError): + getattr(rq, flat_field) + + +class TestPAMUniversalSyncConfigEditCommand(unittest.TestCase): + def setUp(self): + self.params = mock.MagicMock() + self.network = _make_network_record() + mock.patch('keepercommander.commands.universalsecretsync.get_keeper_tokens', + return_value=FAKE_TOKENS).start() + mock.patch('keepercommander.vault.KeeperRecord.load', return_value=self.network).start() + self.mock_router = mock.patch( + 'keepercommander.commands.universalsecretsync.router_helper.router_configure_universal_sync').start() + self.addCleanup(mock.patch.stopall) + + @staticmethod + def _existing_github_config(repo_names, scope=pam_pb2.REPOSITORY, org_visibility=pam_pb2.PRIVATE, + owner='existing-owner'): + encrypted_owner = crypto.encrypt_aes_v2(owner.encode('utf-8'), RECORD_KEY) + repos_b64 = [utils.base64_url_encode(crypto.encrypt_aes_v2(name.encode('utf-8'), RECORD_KEY)) + for name in repo_names] + return { + 'scope': scope, + 'owner': utils.base64_url_encode(encrypted_owner), + 'organizationVisibility': org_visibility, + 'repos': repos_b64, + } + + def _execute_with_dag(self, existing_config, **kwargs): + dag_class, connection_class, _root = _patch_dag(existing_config) + kwargs.setdefault('network', NETWORK_UID) + with mock.patch('keepercommander.keeper_dag.DAG', dag_class), \ + mock.patch('keepercommander.keeper_dag.connection.commander.Connection', connection_class): + PAMUniversalSyncConfigEditCommand().execute(self.params, **kwargs) + # Guard against the broad try/except in the command silently swallowing a bad mock wire-up. + self.assertEqual(dag_class.call_count, 1) + self.assertEqual(self.mock_router.call_count, 1) + return self.mock_router.call_args[0][1] + + def test_preserves_existing_github_config_when_not_overridden(self): + existing = {'github': self._existing_github_config(['repo-1', 'repo-2'])} + rq = self._execute_with_dag(existing) + + self.assertTrue(rq.HasField('github')) + self.assertEqual(rq.github.scope, pam_pb2.REPOSITORY) + self.assertEqual(rq.github.organizationVisibility, pam_pb2.PRIVATE) + self.assertEqual(crypto.decrypt_aes_v2(rq.github.owner, RECORD_KEY), b'existing-owner') + decrypted = [crypto.decrypt_aes_v2(r.name, RECORD_KEY).decode('utf-8') for r in rq.github.repos] + self.assertEqual(decrypted, ['repo-1', 'repo-2']) + + def test_overrides_take_precedence_over_existing(self): + existing = {'github': self._existing_github_config(['old-repo'])} + rq = self._execute_with_dag(existing, scope='organization', owner='new-owner', + org_visibility='all', repo=['new-repo']) + + self.assertEqual(rq.github.scope, pam_pb2.ORGANIZATION) + self.assertEqual(rq.github.organizationVisibility, pam_pb2.ALL) + self.assertEqual(crypto.decrypt_aes_v2(rq.github.owner, RECORD_KEY), b'new-owner') + decrypted = [crypto.decrypt_aes_v2(r.name, RECORD_KEY).decode('utf-8') for r in rq.github.repos] + self.assertEqual(decrypted, ['new-repo']) + + def test_partial_override_preserves_the_rest(self): + # Only --owner is overridden; scope/org-visibility/repos should carry forward. + existing = {'github': self._existing_github_config(['repo-1'], scope=pam_pb2.ORGANIZATION, + org_visibility=pam_pb2.SELECTED)} + rq = self._execute_with_dag(existing, owner='new-owner') + + self.assertEqual(crypto.decrypt_aes_v2(rq.github.owner, RECORD_KEY), b'new-owner') + self.assertEqual(rq.github.scope, pam_pb2.ORGANIZATION) + self.assertEqual(rq.github.organizationVisibility, pam_pb2.SELECTED) + decrypted = [crypto.decrypt_aes_v2(r.name, RECORD_KEY).decode('utf-8') for r in rq.github.repos] + self.assertEqual(decrypted, ['repo-1']) + + def test_no_existing_and_no_override_leaves_github_unset(self): + rq = self._execute_with_dag(None) + self.assertFalse(rq.HasField('github')) + + +class TestPAMUniversalSyncConfigListDetails(unittest.TestCase): + def setUp(self): + self.params = mock.MagicMock() + self.params.folder_cache = {} + self.params.subfolder_cache = {} + self.network = _make_network_record() + mock.patch('keepercommander.vault.KeeperRecord.load', return_value=self.network).start() + self.addCleanup(mock.patch.stopall) + + def _print_details(self, existing_config, format_type='json'): + dag_class, connection_class, _root = _patch_dag(existing_config) + with mock.patch('keepercommander.keeper_dag.DAG', dag_class), \ + mock.patch('keepercommander.keeper_dag.connection.commander.Connection', connection_class): + return PAMUniversalSyncConfigListCommand.print_uss_configuration_details( + self.params, NETWORK_UID, format_type=format_type) + + def test_json_details_reads_nested_github_fields(self): + encrypted_owner = crypto.encrypt_aes_v2(b'my-org', RECORD_KEY) + encrypted_repo = crypto.encrypt_aes_v2(b'my-repo', RECORD_KEY) + existing_config = { + 'enabled': True, + 'github': { + 'scope': pam_pb2.ORGANIZATION, + 'owner': utils.base64_url_encode(encrypted_owner), + 'organizationVisibility': pam_pb2.ALL, + 'repos': [utils.base64_url_encode(encrypted_repo)], + }, + } + + result = json.loads(self._print_details(existing_config)) + + self.assertEqual(result['scope'], 'ORGANIZATION') + self.assertEqual(result['organization_visibility'], 'ALL') + self.assertEqual(result['owner'], 'my-org') + self.assertEqual(result['repos'], ['my-repo']) + + def test_json_details_without_github_config(self): + result = json.loads(self._print_details({'enabled': True})) + + self.assertEqual(result['scope'], 'N/A') + self.assertEqual(result['organization_visibility'], 'N/A') + self.assertEqual(result['owner'], 'N/A') + self.assertEqual(result['repos'], []) + + +if __name__ == '__main__': + unittest.main()