catocli 2.1.0__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of catocli might be problematic. Click here for more details.
- catocli/Utils/clidriver.py +22 -4
- catocli/Utils/version_checker.py +1 -1
- catocli/__init__.py +1 -1
- catocli/parsers/custom/import_rules_to_tf/import_rules_to_tf.py +13 -2
- catocli/parsers/custom/import_sites_to_tf/import_sites_to_tf.py +8 -1
- catocli/parsers/custom_private/__init__.py +134 -0
- catocli/parsers/parserApiClient.py +427 -1
- {catocli-2.1.0.dist-info → catocli-2.1.1.dist-info}/METADATA +1 -1
- {catocli-2.1.0.dist-info → catocli-2.1.1.dist-info}/RECORD +15 -14
- graphql_client/api/call_api.py +20 -2
- schema/catolib.py +22 -4
- {catocli-2.1.0.dist-info → catocli-2.1.1.dist-info}/WHEEL +0 -0
- {catocli-2.1.0.dist-info → catocli-2.1.1.dist-info}/entry_points.txt +0 -0
- {catocli-2.1.0.dist-info → catocli-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {catocli-2.1.0.dist-info → catocli-2.1.1.dist-info}/top_level.txt +0 -0
catocli/Utils/clidriver.py
CHANGED
|
@@ -18,6 +18,7 @@ profile_manager = get_profile_manager()
|
|
|
18
18
|
CATO_DEBUG = bool(os.getenv("CATO_DEBUG", False))
|
|
19
19
|
from ..parsers.raw import raw_parse
|
|
20
20
|
from ..parsers.custom import custom_parse
|
|
21
|
+
from ..parsers.custom_private import private_parse
|
|
21
22
|
from ..parsers.query_siteLocation import query_siteLocation_parse
|
|
22
23
|
from ..parsers.mutation_accountManagement import mutation_accountManagement_parse
|
|
23
24
|
from ..parsers.mutation_admin import mutation_admin_parse
|
|
@@ -82,6 +83,15 @@ def show_version_info(args, configuration=None):
|
|
|
82
83
|
print("Unable to check for updates (check your internet connection)")
|
|
83
84
|
return [{"success": True, "current_version": catocli.__version__, "latest_version": latest_version if not args.current_only else None}]
|
|
84
85
|
|
|
86
|
+
def load_private_settings():
|
|
87
|
+
# Load private settings from ~/.cato/settings.json
|
|
88
|
+
settings_file = os.path.expanduser("~/.cato/settings.json")
|
|
89
|
+
try:
|
|
90
|
+
with open(settings_file, 'r') as f:
|
|
91
|
+
return json.load(f)
|
|
92
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
93
|
+
return {}
|
|
94
|
+
|
|
85
95
|
def get_configuration(skip_api_key=False):
|
|
86
96
|
configuration = Configuration()
|
|
87
97
|
configuration.verify_ssl = False
|
|
@@ -104,10 +114,13 @@ def get_configuration(skip_api_key=False):
|
|
|
104
114
|
print(f"Run 'catocli configure set --profile {profile_name}' to update your credentials.")
|
|
105
115
|
exit(1)
|
|
106
116
|
|
|
117
|
+
# Use standard endpoint from profile for regular API calls
|
|
118
|
+
configuration.host = credentials['endpoint']
|
|
119
|
+
|
|
107
120
|
# Only set API key if not using custom headers file
|
|
121
|
+
# (Private settings are handled separately in createPrivateRequest)
|
|
108
122
|
if not skip_api_key:
|
|
109
123
|
configuration.api_key["x-api-key"] = credentials['cato_token']
|
|
110
|
-
configuration.host = credentials['endpoint']
|
|
111
124
|
configuration.accountID = credentials['account_id']
|
|
112
125
|
|
|
113
126
|
return configuration
|
|
@@ -136,6 +149,7 @@ version_parser.add_argument('--current-only', action='store_true', help='Show on
|
|
|
136
149
|
version_parser.set_defaults(func=show_version_info)
|
|
137
150
|
|
|
138
151
|
custom_parsers = custom_parse(subparsers)
|
|
152
|
+
private_parsers = private_parse(subparsers)
|
|
139
153
|
raw_parsers = subparsers.add_parser('raw', help='Raw GraphQL', usage=get_help("raw"))
|
|
140
154
|
raw_parser = raw_parse(raw_parsers)
|
|
141
155
|
query_parser = subparsers.add_parser('query', help='Query', usage='catocli query <operationName> [options]')
|
|
@@ -239,6 +253,7 @@ def main(args=None):
|
|
|
239
253
|
response = args.func(args, None)
|
|
240
254
|
else:
|
|
241
255
|
# Check if using headers file to determine if we should skip API key
|
|
256
|
+
# Note: Private settings should NOT affect regular API calls - only private commands
|
|
242
257
|
using_headers_file = hasattr(args, 'headers_file') and args.headers_file
|
|
243
258
|
|
|
244
259
|
# Get configuration from profiles
|
|
@@ -252,8 +267,8 @@ def main(args=None):
|
|
|
252
267
|
custom_headers.update(parse_headers_from_file(args.headers_file))
|
|
253
268
|
if custom_headers:
|
|
254
269
|
configuration.custom_headers.update(custom_headers)
|
|
255
|
-
# Handle account ID override
|
|
256
|
-
if args.func.__name__
|
|
270
|
+
# Handle account ID override (applies to all commands except raw)
|
|
271
|
+
if args.func.__name__ not in ["createRawRequest"]:
|
|
257
272
|
if hasattr(args, 'accountID') and args.accountID is not None:
|
|
258
273
|
# Command line override takes precedence
|
|
259
274
|
configuration.accountID = args.accountID
|
|
@@ -266,6 +281,9 @@ def main(args=None):
|
|
|
266
281
|
else:
|
|
267
282
|
if response!=None:
|
|
268
283
|
print(json.dumps(response[0], sort_keys=True, indent=4))
|
|
284
|
+
except KeyboardInterrupt:
|
|
285
|
+
print('Operation cancelled by user (Ctrl+C).')
|
|
286
|
+
exit(130) # Standard exit code for SIGINT
|
|
269
287
|
except Exception as e:
|
|
270
288
|
if isinstance(e, AttributeError):
|
|
271
289
|
print('Missing arguments. Usage: catocli <operation> -h')
|
|
@@ -275,4 +293,4 @@ def main(args=None):
|
|
|
275
293
|
else:
|
|
276
294
|
print('ERROR: ',e)
|
|
277
295
|
traceback.print_exc()
|
|
278
|
-
|
|
296
|
+
exit(1)
|
catocli/Utils/version_checker.py
CHANGED
|
@@ -14,7 +14,7 @@ from .. import __version__
|
|
|
14
14
|
|
|
15
15
|
# Cache settings
|
|
16
16
|
CACHE_FILE = os.path.expanduser("~/.catocli_version_cache")
|
|
17
|
-
CACHE_DURATION = 3600 *
|
|
17
|
+
CACHE_DURATION = 3600 * 4 # 4 hours in seconds
|
|
18
18
|
|
|
19
19
|
def get_cached_version_info():
|
|
20
20
|
"""Get cached version information if still valid"""
|
catocli/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "2.1.
|
|
1
|
+
__version__ = "2.1.1"
|
|
2
2
|
__cato_host__ = "https://api.catonetworks.com/api/v1/graphql2"
|
|
@@ -107,6 +107,9 @@ def run_terraform_import(resource_address, resource_id, timeout=60, verbose=Fals
|
|
|
107
107
|
print(f"Error: {result.stderr}")
|
|
108
108
|
return False, result.stdout, result.stderr
|
|
109
109
|
|
|
110
|
+
except KeyboardInterrupt:
|
|
111
|
+
print(f"\nImport cancelled by user (Ctrl+C)")
|
|
112
|
+
raise # Re-raise to allow higher-level handling
|
|
110
113
|
except subprocess.TimeoutExpired:
|
|
111
114
|
print(f"Timeout: {resource_address} (exceeded {timeout}s)")
|
|
112
115
|
return False, "", f"Command timed out after {timeout} seconds"
|
|
@@ -267,7 +270,11 @@ def import_if_rules_to_tf(args, configuration):
|
|
|
267
270
|
"total_failed": total_failed,
|
|
268
271
|
"module_name": args.module_name
|
|
269
272
|
}]
|
|
270
|
-
|
|
273
|
+
|
|
274
|
+
except KeyboardInterrupt:
|
|
275
|
+
print("\nImport process cancelled by user (Ctrl+C).")
|
|
276
|
+
print("Partial imports may have been completed.")
|
|
277
|
+
return [{"success": False, "error": "Import cancelled by user"}]
|
|
271
278
|
except Exception as e:
|
|
272
279
|
print(f"ERROR: {str(e)}")
|
|
273
280
|
return [{"success": False, "error": str(e)}]
|
|
@@ -445,7 +452,11 @@ def import_wf_rules_to_tf(args, configuration):
|
|
|
445
452
|
"total_failed": total_failed,
|
|
446
453
|
"module_name": args.module_name
|
|
447
454
|
}]
|
|
448
|
-
|
|
455
|
+
|
|
456
|
+
except KeyboardInterrupt:
|
|
457
|
+
print("\nImport process cancelled by user (Ctrl+C).")
|
|
458
|
+
print("Partial imports may have been completed.")
|
|
459
|
+
return [{"success": False, "error": "Import cancelled by user"}]
|
|
449
460
|
except Exception as e:
|
|
450
461
|
print(f"ERROR: {str(e)}")
|
|
451
462
|
return [{"success": False, "error": str(e)}]
|
|
@@ -174,6 +174,9 @@ def run_terraform_import(resource_address, resource_id, timeout=60, verbose=Fals
|
|
|
174
174
|
print(f"Error: {result.stderr}")
|
|
175
175
|
return False, result.stdout, result.stderr
|
|
176
176
|
|
|
177
|
+
except KeyboardInterrupt:
|
|
178
|
+
print(f"\nImport cancelled by user (Ctrl+C)")
|
|
179
|
+
raise # Re-raise to allow higher-level handling
|
|
177
180
|
except subprocess.TimeoutExpired:
|
|
178
181
|
print(f"Timeout: {resource_address} (exceeded {timeout}s)")
|
|
179
182
|
return False, "", f"Command timed out after {timeout} seconds"
|
|
@@ -951,7 +954,11 @@ def import_socket_sites_to_tf(args, configuration):
|
|
|
951
954
|
"total_failed": total_failed,
|
|
952
955
|
"module_name": args.module_name
|
|
953
956
|
}]
|
|
954
|
-
|
|
957
|
+
|
|
958
|
+
except KeyboardInterrupt:
|
|
959
|
+
print("\nImport process cancelled by user (Ctrl+C).")
|
|
960
|
+
print("Partial imports may have been completed.")
|
|
961
|
+
return [{"success": False, "error": "Import cancelled by user"}]
|
|
955
962
|
except Exception as e:
|
|
956
963
|
print(f"ERROR: {str(e)}")
|
|
957
964
|
return [{"success": False, "error": str(e)}]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Private commands parser for custom GraphQL payloads
|
|
4
|
+
Dynamically loads commands from ~/.cato/settings.json
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
import argparse
|
|
10
|
+
from ..parserApiClient import createPrivateRequest, get_private_help
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_private_settings():
|
|
14
|
+
"""Load private settings from ~/.cato/settings.json"""
|
|
15
|
+
settings_file = os.path.expanduser("~/.cato/settings.json")
|
|
16
|
+
try:
|
|
17
|
+
with open(settings_file, 'r') as f:
|
|
18
|
+
settings = json.load(f)
|
|
19
|
+
return settings.get('privateCommands', {})
|
|
20
|
+
except (FileNotFoundError, json.JSONDecodeError, KeyError):
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def private_parse(subparsers):
|
|
25
|
+
"""Check for private settings and create private parser if found"""
|
|
26
|
+
private_commands = load_private_settings()
|
|
27
|
+
|
|
28
|
+
if not private_commands:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
# Create the private subparser
|
|
32
|
+
private_parser = subparsers.add_parser(
|
|
33
|
+
'private',
|
|
34
|
+
help='Private custom commands (configured in ~/.cato/settings.json)',
|
|
35
|
+
usage='catocli private <command> [options]'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
private_subparsers = private_parser.add_subparsers(
|
|
39
|
+
description='Available private commands',
|
|
40
|
+
help='Private command help'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Dynamically create subparsers for each private command
|
|
44
|
+
for command_name, command_config in private_commands.items():
|
|
45
|
+
create_private_command_parser(
|
|
46
|
+
private_subparsers,
|
|
47
|
+
command_name,
|
|
48
|
+
command_config
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return private_parser
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_private_command_parser(subparsers, command_name, command_config):
|
|
55
|
+
"""Create a parser for a specific private command"""
|
|
56
|
+
|
|
57
|
+
# Create the command parser
|
|
58
|
+
cmd_parser = subparsers.add_parser(
|
|
59
|
+
command_name,
|
|
60
|
+
help=f'Execute private command: {command_name}',
|
|
61
|
+
usage=get_private_help(command_name, command_config)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Add standard arguments
|
|
65
|
+
cmd_parser.add_argument(
|
|
66
|
+
'json',
|
|
67
|
+
nargs='?',
|
|
68
|
+
default='{}',
|
|
69
|
+
help='Variables in JSON format (defaults to empty object if not provided).'
|
|
70
|
+
)
|
|
71
|
+
cmd_parser.add_argument(
|
|
72
|
+
'-t',
|
|
73
|
+
const=True,
|
|
74
|
+
default=False,
|
|
75
|
+
nargs='?',
|
|
76
|
+
help='Print GraphQL query without sending API call'
|
|
77
|
+
)
|
|
78
|
+
cmd_parser.add_argument(
|
|
79
|
+
'-v',
|
|
80
|
+
const=True,
|
|
81
|
+
default=False,
|
|
82
|
+
nargs='?',
|
|
83
|
+
help='Verbose output'
|
|
84
|
+
)
|
|
85
|
+
cmd_parser.add_argument(
|
|
86
|
+
'-p',
|
|
87
|
+
const=True,
|
|
88
|
+
default=False,
|
|
89
|
+
nargs='?',
|
|
90
|
+
help='Pretty print'
|
|
91
|
+
)
|
|
92
|
+
cmd_parser.add_argument(
|
|
93
|
+
'-H', '--header',
|
|
94
|
+
action='append',
|
|
95
|
+
dest='headers',
|
|
96
|
+
help='Add custom headers in "Key: Value" format. Can be used multiple times.'
|
|
97
|
+
)
|
|
98
|
+
cmd_parser.add_argument(
|
|
99
|
+
'--headers-file',
|
|
100
|
+
dest='headers_file',
|
|
101
|
+
help='Load headers from a file. Each line should contain a header in "Key: Value" format.'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Add standard accountID argument (like other commands)
|
|
105
|
+
cmd_parser.add_argument(
|
|
106
|
+
'-accountID',
|
|
107
|
+
help='Override the account ID from profile with this value.'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Add dynamic arguments based on command configuration (excluding accountId since it's handled above)
|
|
111
|
+
if 'arguments' in command_config:
|
|
112
|
+
for arg in command_config['arguments']:
|
|
113
|
+
arg_name = arg.get('name')
|
|
114
|
+
# Skip accountId since it's handled by the standard -accountID argument
|
|
115
|
+
if arg_name and arg_name.lower() != 'accountid':
|
|
116
|
+
arg_type = arg.get('type', 'string')
|
|
117
|
+
arg_default = arg.get('default')
|
|
118
|
+
arg_help = f"Argument: {arg_name}"
|
|
119
|
+
|
|
120
|
+
if arg_default:
|
|
121
|
+
arg_help += f" (default: {arg_default})"
|
|
122
|
+
|
|
123
|
+
cmd_parser.add_argument(
|
|
124
|
+
f'--{arg_name}',
|
|
125
|
+
help=arg_help,
|
|
126
|
+
default=arg_default
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Set the function to handle this command
|
|
130
|
+
cmd_parser.set_defaults(
|
|
131
|
+
func=createPrivateRequest,
|
|
132
|
+
private_command=command_name,
|
|
133
|
+
private_config=command_config
|
|
134
|
+
)
|
|
@@ -7,6 +7,7 @@ from graphql_client.api_client import ApiException
|
|
|
7
7
|
import logging
|
|
8
8
|
import pprint
|
|
9
9
|
import uuid
|
|
10
|
+
import string
|
|
10
11
|
from urllib3.filepost import encode_multipart_formdata
|
|
11
12
|
|
|
12
13
|
def createRequest(args, configuration):
|
|
@@ -24,7 +25,12 @@ def createRequest(args, configuration):
|
|
|
24
25
|
elif not params["t"] and params["json"] is None:
|
|
25
26
|
# Default to empty object if no json provided and not using -t flag
|
|
26
27
|
variablesObj = {}
|
|
27
|
-
|
|
28
|
+
# Special handling for eventsFeed and auditFeed which use accountIDs array
|
|
29
|
+
if operationName in ["query.eventsFeed", "query.auditFeed"]:
|
|
30
|
+
# Only add accountIDs if not already provided in JSON
|
|
31
|
+
if "accountIDs" not in variablesObj:
|
|
32
|
+
variablesObj["accountIDs"] = [configuration.accountID]
|
|
33
|
+
elif "accountId" in operation["args"]:
|
|
28
34
|
variablesObj["accountId"] = configuration.accountID
|
|
29
35
|
else:
|
|
30
36
|
variablesObj["accountID"] = configuration.accountID
|
|
@@ -438,6 +444,339 @@ def createRawBinaryRequest(args, configuration):
|
|
|
438
444
|
print(f"ERROR: Failed to send multipart request: {error_str}")
|
|
439
445
|
return None
|
|
440
446
|
|
|
447
|
+
def get_private_help(command_name, command_config):
|
|
448
|
+
"""Generate comprehensive help text for a private command"""
|
|
449
|
+
usage = f"catocli private {command_name}"
|
|
450
|
+
|
|
451
|
+
# Create comprehensive JSON example with all arguments (excluding accountId)
|
|
452
|
+
if 'arguments' in command_config:
|
|
453
|
+
json_example = {}
|
|
454
|
+
for arg in command_config['arguments']:
|
|
455
|
+
arg_name = arg.get('name')
|
|
456
|
+
# Skip accountId since it's handled by standard -accountID CLI argument
|
|
457
|
+
if arg_name and arg_name.lower() != 'accountid':
|
|
458
|
+
if 'example' in arg:
|
|
459
|
+
# Use explicit example if provided
|
|
460
|
+
json_example[arg_name] = arg['example']
|
|
461
|
+
elif 'default' in arg:
|
|
462
|
+
# Use default value if available
|
|
463
|
+
json_example[arg_name] = arg['default']
|
|
464
|
+
else:
|
|
465
|
+
# Generate placeholder based on type
|
|
466
|
+
arg_type = arg.get('type', 'string')
|
|
467
|
+
if arg_type == 'string':
|
|
468
|
+
json_example[arg_name] = f"<{arg_name}>"
|
|
469
|
+
elif arg_type == 'object':
|
|
470
|
+
if 'struct' in arg:
|
|
471
|
+
# Use struct definition
|
|
472
|
+
json_example[arg_name] = arg['struct']
|
|
473
|
+
else:
|
|
474
|
+
json_example[arg_name] = {}
|
|
475
|
+
else:
|
|
476
|
+
json_example[arg_name] = f"<{arg_name}>"
|
|
477
|
+
|
|
478
|
+
if json_example:
|
|
479
|
+
# Format JSON nicely for readability in help
|
|
480
|
+
json_str = json.dumps(json_example, indent=2)
|
|
481
|
+
usage += f" '{json_str}'"
|
|
482
|
+
|
|
483
|
+
# Add common options
|
|
484
|
+
usage += " [-t] [-v] [-p]"
|
|
485
|
+
|
|
486
|
+
# Add command-specific arguments with descriptions (excluding accountId)
|
|
487
|
+
if 'arguments' in command_config:
|
|
488
|
+
filtered_args = [arg for arg in command_config['arguments'] if arg.get('name', '').lower() != 'accountid']
|
|
489
|
+
if filtered_args:
|
|
490
|
+
usage += "\n\nArguments:"
|
|
491
|
+
for arg in filtered_args:
|
|
492
|
+
arg_name = arg.get('name')
|
|
493
|
+
arg_type = arg.get('type', 'string')
|
|
494
|
+
arg_default = arg.get('default')
|
|
495
|
+
arg_example = arg.get('example')
|
|
496
|
+
|
|
497
|
+
if arg_name:
|
|
498
|
+
usage += f"\n --{arg_name}: {arg_type}"
|
|
499
|
+
if arg_default is not None:
|
|
500
|
+
usage += f" (default: {arg_default})"
|
|
501
|
+
if arg_example is not None and arg_example != arg_default:
|
|
502
|
+
usage += f" (example: {json.dumps(arg_example) if isinstance(arg_example, (dict, list)) else arg_example})"
|
|
503
|
+
|
|
504
|
+
# Add standard accountID information
|
|
505
|
+
usage += "\n\nStandard Arguments:"
|
|
506
|
+
usage += "\n -accountID: Account ID (taken from profile, can be overridden)"
|
|
507
|
+
|
|
508
|
+
# Add payload file info if available
|
|
509
|
+
if 'payloadFilePath' in command_config:
|
|
510
|
+
usage += f"\n\nPayload template: {command_config['payloadFilePath']}"
|
|
511
|
+
|
|
512
|
+
# Add batch processing info if configured
|
|
513
|
+
if 'batchSize' in command_config:
|
|
514
|
+
usage += f"\nBatch size: {command_config['batchSize']}"
|
|
515
|
+
if 'paginationParam' in command_config:
|
|
516
|
+
usage += f" (pagination: {command_config['paginationParam']})"
|
|
517
|
+
|
|
518
|
+
return usage
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def load_payload_template(command_config):
|
|
522
|
+
"""Load and return the GraphQL payload template for a private command"""
|
|
523
|
+
try:
|
|
524
|
+
payload_path = command_config.get('payloadFilePath')
|
|
525
|
+
if not payload_path:
|
|
526
|
+
raise ValueError(f"Missing payloadFilePath in command configuration")
|
|
527
|
+
|
|
528
|
+
# Construct the full path relative to the settings directory
|
|
529
|
+
settings_dir = os.path.expanduser("~/.cato")
|
|
530
|
+
full_payload_path = os.path.join(settings_dir, payload_path)
|
|
531
|
+
|
|
532
|
+
# Load the payload file using the standard JSON loading mechanism
|
|
533
|
+
try:
|
|
534
|
+
with open(full_payload_path, 'r') as f:
|
|
535
|
+
return json.load(f)
|
|
536
|
+
except FileNotFoundError:
|
|
537
|
+
raise ValueError(f"Payload file not found: {full_payload_path}")
|
|
538
|
+
except json.JSONDecodeError as e:
|
|
539
|
+
raise ValueError(f"Invalid JSON in payload file {full_payload_path}: {e}")
|
|
540
|
+
except Exception as e:
|
|
541
|
+
raise ValueError(f"Failed to load payload template: {e}")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def set_nested_value(obj, path, value):
|
|
545
|
+
"""Set a value at a nested path in an object using jQuery-style JSON path syntax
|
|
546
|
+
|
|
547
|
+
Supports:
|
|
548
|
+
- Simple dot notation: 'a.b.c'
|
|
549
|
+
- Array access: 'a.b[0].c' or 'a.b[0]'
|
|
550
|
+
- Mixed paths: 'variables.filters[0].search'
|
|
551
|
+
- Deep nesting: 'data.results[0].items[1].properties.name'
|
|
552
|
+
"""
|
|
553
|
+
import re
|
|
554
|
+
|
|
555
|
+
# Parse the path into components handling both dot notation and array indices
|
|
556
|
+
# Split by dots first, then handle array indices
|
|
557
|
+
path_parts = []
|
|
558
|
+
for part in path.split('.'):
|
|
559
|
+
# Check if this part contains array notation like 'items[0]'
|
|
560
|
+
array_matches = re.findall(r'([^\[]+)(?:\[(\d+)\])?', part)
|
|
561
|
+
for match in array_matches:
|
|
562
|
+
key, index = match
|
|
563
|
+
if key: # Add the key part
|
|
564
|
+
path_parts.append(key)
|
|
565
|
+
if index: # Add the array index part
|
|
566
|
+
path_parts.append(int(index))
|
|
567
|
+
|
|
568
|
+
current = obj
|
|
569
|
+
|
|
570
|
+
# Navigate to the parent of the target location
|
|
571
|
+
for i, part in enumerate(path_parts[:-1]):
|
|
572
|
+
next_part = path_parts[i + 1]
|
|
573
|
+
|
|
574
|
+
if isinstance(part, int):
|
|
575
|
+
# Current part is an array index
|
|
576
|
+
if not isinstance(current, list):
|
|
577
|
+
raise ValueError(f"Expected array at path component {i}, got {type(current).__name__}")
|
|
578
|
+
|
|
579
|
+
# Extend array if necessary
|
|
580
|
+
while len(current) <= part:
|
|
581
|
+
current.append(None)
|
|
582
|
+
|
|
583
|
+
# Initialize the array element if it doesn't exist
|
|
584
|
+
if current[part] is None:
|
|
585
|
+
if isinstance(next_part, int):
|
|
586
|
+
current[part] = [] # Next part is array index, so create array
|
|
587
|
+
else:
|
|
588
|
+
current[part] = {} # Next part is object key, so create object
|
|
589
|
+
|
|
590
|
+
current = current[part]
|
|
591
|
+
|
|
592
|
+
else:
|
|
593
|
+
# Current part is an object key
|
|
594
|
+
if not isinstance(current, dict):
|
|
595
|
+
raise ValueError(f"Expected object at path component {i}, got {type(current).__name__}")
|
|
596
|
+
|
|
597
|
+
# Create the key if it doesn't exist
|
|
598
|
+
if part not in current:
|
|
599
|
+
if isinstance(next_part, int):
|
|
600
|
+
current[part] = [] # Next part is array index, so create array
|
|
601
|
+
else:
|
|
602
|
+
current[part] = {} # Next part is object key, so create object
|
|
603
|
+
|
|
604
|
+
current = current[part]
|
|
605
|
+
|
|
606
|
+
# Set the final value
|
|
607
|
+
final_part = path_parts[-1]
|
|
608
|
+
if isinstance(final_part, int):
|
|
609
|
+
# Final part is an array index
|
|
610
|
+
if not isinstance(current, list):
|
|
611
|
+
raise ValueError(f"Expected array at final path component, got {type(current).__name__}")
|
|
612
|
+
|
|
613
|
+
# Extend array if necessary
|
|
614
|
+
while len(current) <= final_part:
|
|
615
|
+
current.append(None)
|
|
616
|
+
|
|
617
|
+
current[final_part] = value
|
|
618
|
+
else:
|
|
619
|
+
# Final part is an object key
|
|
620
|
+
if not isinstance(current, dict):
|
|
621
|
+
raise ValueError(f"Expected object at final path component, got {type(current).__name__}")
|
|
622
|
+
|
|
623
|
+
current[final_part] = value
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def apply_template_variables(template, variables, private_config):
|
|
627
|
+
"""Apply variables to the template using path-based insertion and template replacement"""
|
|
628
|
+
if not template or not isinstance(template, dict):
|
|
629
|
+
return template
|
|
630
|
+
|
|
631
|
+
# Make a deep copy to avoid modifying the original
|
|
632
|
+
import copy
|
|
633
|
+
result = copy.deepcopy(template)
|
|
634
|
+
|
|
635
|
+
# First, handle path-based variable insertion from private_config
|
|
636
|
+
if private_config and 'arguments' in private_config:
|
|
637
|
+
for arg in private_config['arguments']:
|
|
638
|
+
arg_name = arg.get('name')
|
|
639
|
+
arg_paths = arg.get('path', [])
|
|
640
|
+
|
|
641
|
+
if arg_name and arg_name in variables and arg_paths:
|
|
642
|
+
# Insert the variable value at each specified path
|
|
643
|
+
for path in arg_paths:
|
|
644
|
+
try:
|
|
645
|
+
set_nested_value(result, path, variables[arg_name])
|
|
646
|
+
except Exception as e:
|
|
647
|
+
# If path insertion fails, continue to template replacement
|
|
648
|
+
pass
|
|
649
|
+
|
|
650
|
+
# Second, handle traditional template variable replacement as fallback
|
|
651
|
+
def traverse_and_replace(obj, path=""):
|
|
652
|
+
if isinstance(obj, dict):
|
|
653
|
+
for key, value in list(obj.items()):
|
|
654
|
+
new_path = f"{path}.{key}" if path else key
|
|
655
|
+
|
|
656
|
+
# Check if this is a template variable (string that starts with '{{')
|
|
657
|
+
if isinstance(value, str) and value.startswith('{{') and value.endswith('}}'):
|
|
658
|
+
# Extract variable name
|
|
659
|
+
var_name = value[2:-2].strip()
|
|
660
|
+
|
|
661
|
+
# Replace with actual value if available
|
|
662
|
+
if var_name in variables:
|
|
663
|
+
obj[key] = variables[var_name]
|
|
664
|
+
|
|
665
|
+
# Recursively process nested objects
|
|
666
|
+
else:
|
|
667
|
+
traverse_and_replace(value, new_path)
|
|
668
|
+
|
|
669
|
+
elif isinstance(obj, list):
|
|
670
|
+
for i, item in enumerate(obj):
|
|
671
|
+
traverse_and_replace(item, f"{path}[{i}]")
|
|
672
|
+
|
|
673
|
+
traverse_and_replace(result)
|
|
674
|
+
return result
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def createPrivateRequest(args, configuration):
|
|
678
|
+
"""Handle private command execution using GraphQL payload templates"""
|
|
679
|
+
params = vars(args)
|
|
680
|
+
|
|
681
|
+
# Get the private command configuration
|
|
682
|
+
private_command = params.get('private_command')
|
|
683
|
+
private_config = params.get('private_config')
|
|
684
|
+
|
|
685
|
+
if not private_command or not private_config:
|
|
686
|
+
print("ERROR: Missing private command configuration")
|
|
687
|
+
return None
|
|
688
|
+
|
|
689
|
+
# Load private settings and apply ONLY for private commands
|
|
690
|
+
try:
|
|
691
|
+
settings_file = os.path.expanduser("~/.cato/settings.json")
|
|
692
|
+
with open(settings_file, 'r') as f:
|
|
693
|
+
private_settings = json.load(f)
|
|
694
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
695
|
+
private_settings = {}
|
|
696
|
+
|
|
697
|
+
# Override endpoint if specified in private settings
|
|
698
|
+
if 'baseUrl' in private_settings:
|
|
699
|
+
configuration.host = private_settings['baseUrl']
|
|
700
|
+
|
|
701
|
+
# Add custom headers from private settings
|
|
702
|
+
if 'headers' in private_settings and isinstance(private_settings['headers'], dict):
|
|
703
|
+
if not hasattr(configuration, 'custom_headers'):
|
|
704
|
+
configuration.custom_headers = {}
|
|
705
|
+
for key, value in private_settings['headers'].items():
|
|
706
|
+
configuration.custom_headers[key] = value
|
|
707
|
+
|
|
708
|
+
# Parse input JSON variables
|
|
709
|
+
try:
|
|
710
|
+
variables = json.loads(params.get('json', '{}'))
|
|
711
|
+
except ValueError as e:
|
|
712
|
+
print(f"ERROR: Invalid JSON input: {e}")
|
|
713
|
+
return None
|
|
714
|
+
|
|
715
|
+
# Apply default values from settings configuration first
|
|
716
|
+
for arg in private_config.get('arguments', []):
|
|
717
|
+
arg_name = arg.get('name')
|
|
718
|
+
if arg_name and 'default' in arg:
|
|
719
|
+
variables[arg_name] = arg['default']
|
|
720
|
+
|
|
721
|
+
# Apply profile account ID as fallback (lower priority than settings defaults)
|
|
722
|
+
# Only apply if accountId is not already set by settings defaults
|
|
723
|
+
if configuration and hasattr(configuration, 'accountID'):
|
|
724
|
+
if 'accountID' not in variables and 'accountId' not in variables:
|
|
725
|
+
# Use both naming conventions to support different payload templates
|
|
726
|
+
variables['accountID'] = configuration.accountID
|
|
727
|
+
variables['accountId'] = configuration.accountID
|
|
728
|
+
# If accountId/accountID exists but not the other variation, add both
|
|
729
|
+
elif 'accountID' in variables and 'accountId' not in variables:
|
|
730
|
+
variables['accountId'] = variables['accountID']
|
|
731
|
+
elif 'accountId' in variables and 'accountID' not in variables:
|
|
732
|
+
variables['accountID'] = variables['accountId']
|
|
733
|
+
|
|
734
|
+
# Apply CLI argument values (highest priority - overrides everything)
|
|
735
|
+
for arg in private_config.get('arguments', []):
|
|
736
|
+
arg_name = arg.get('name')
|
|
737
|
+
if arg_name:
|
|
738
|
+
# Handle special case for accountId - CLI uses -accountID but config uses accountId
|
|
739
|
+
if arg_name.lower() == 'accountid':
|
|
740
|
+
if hasattr(args, 'accountID') and getattr(args, 'accountID') is not None:
|
|
741
|
+
arg_value = getattr(args, 'accountID')
|
|
742
|
+
variables['accountID'] = arg_value
|
|
743
|
+
variables['accountId'] = arg_value
|
|
744
|
+
elif hasattr(args, 'accountId') and getattr(args, 'accountId') is not None:
|
|
745
|
+
arg_value = getattr(args, 'accountId')
|
|
746
|
+
variables['accountID'] = arg_value
|
|
747
|
+
variables['accountId'] = arg_value
|
|
748
|
+
# Handle other arguments normally
|
|
749
|
+
else:
|
|
750
|
+
if hasattr(args, arg_name):
|
|
751
|
+
arg_value = getattr(args, arg_name)
|
|
752
|
+
if arg_value is not None:
|
|
753
|
+
variables[arg_name] = arg_value
|
|
754
|
+
|
|
755
|
+
# Load the payload template
|
|
756
|
+
try:
|
|
757
|
+
payload_template = load_payload_template(private_config)
|
|
758
|
+
except ValueError as e:
|
|
759
|
+
print(f"ERROR: {e}")
|
|
760
|
+
return None
|
|
761
|
+
|
|
762
|
+
# Apply variables to the template using path-based insertion
|
|
763
|
+
body = apply_template_variables(payload_template, variables, private_config)
|
|
764
|
+
|
|
765
|
+
# Test mode - just print the request
|
|
766
|
+
if params.get('t'):
|
|
767
|
+
if params.get('p'):
|
|
768
|
+
print(json.dumps(body, indent=2, sort_keys=True))
|
|
769
|
+
else:
|
|
770
|
+
print(json.dumps(body))
|
|
771
|
+
return None
|
|
772
|
+
|
|
773
|
+
# Execute the GraphQL request using custom method (no User-Agent header)
|
|
774
|
+
try:
|
|
775
|
+
return sendPrivateGraphQLRequest(configuration, body, params)
|
|
776
|
+
except Exception as e:
|
|
777
|
+
return e
|
|
778
|
+
|
|
779
|
+
|
|
441
780
|
def sendMultipartRequest(configuration, form_fields, files, params):
|
|
442
781
|
"""Send a multipart/form-data request directly using urllib3"""
|
|
443
782
|
import urllib3
|
|
@@ -520,3 +859,90 @@ def sendMultipartRequest(configuration, form_fields, files, params):
|
|
|
520
859
|
error_str = f"Exception of type {type(e).__name__}"
|
|
521
860
|
print(f"ERROR: Network/request error: {error_str}")
|
|
522
861
|
return None
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def sendPrivateGraphQLRequest(configuration, body, params):
|
|
865
|
+
"""Send a GraphQL request for private commands without User-Agent header"""
|
|
866
|
+
import urllib3
|
|
867
|
+
|
|
868
|
+
# Create pool manager
|
|
869
|
+
pool_manager = urllib3.PoolManager(
|
|
870
|
+
cert_reqs='CERT_NONE' if not getattr(configuration, 'verify_ssl', False) else 'CERT_REQUIRED'
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
# Prepare headers WITHOUT User-Agent
|
|
874
|
+
headers = {
|
|
875
|
+
'Content-Type': 'application/json'
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
# Add API key if not using headers file or custom headers
|
|
879
|
+
using_custom_headers = hasattr(configuration, 'custom_headers') and configuration.custom_headers
|
|
880
|
+
if not using_custom_headers and hasattr(configuration, 'api_key') and configuration.api_key and 'x-api-key' in configuration.api_key:
|
|
881
|
+
headers['x-api-key'] = configuration.api_key['x-api-key']
|
|
882
|
+
|
|
883
|
+
# Add custom headers
|
|
884
|
+
if using_custom_headers:
|
|
885
|
+
headers.update(configuration.custom_headers)
|
|
886
|
+
|
|
887
|
+
# Encode headers to handle Unicode characters properly
|
|
888
|
+
encoded_headers = {}
|
|
889
|
+
for key, value in headers.items():
|
|
890
|
+
# Ensure header values are properly encoded as strings
|
|
891
|
+
if isinstance(value, str):
|
|
892
|
+
# Replace problematic Unicode characters that can't be encoded in latin-1
|
|
893
|
+
value = value.encode('utf-8', errors='replace').decode('latin-1', errors='replace')
|
|
894
|
+
encoded_headers[key] = value
|
|
895
|
+
headers = encoded_headers
|
|
896
|
+
|
|
897
|
+
# Verbose output
|
|
898
|
+
if params.get("v") == True:
|
|
899
|
+
print(f"Host: {getattr(configuration, 'host', 'unknown')}")
|
|
900
|
+
masked_headers = headers.copy()
|
|
901
|
+
if 'x-api-key' in masked_headers:
|
|
902
|
+
masked_headers['x-api-key'] = '***MASKED***'
|
|
903
|
+
# Also mask Cookie for privacy
|
|
904
|
+
if 'Cookie' in masked_headers:
|
|
905
|
+
masked_headers['Cookie'] = '***MASKED***'
|
|
906
|
+
print(f"Request Headers: {json.dumps(masked_headers, indent=4, sort_keys=True)}")
|
|
907
|
+
print(f"Request Data: {json.dumps(body, indent=4, sort_keys=True)}\n")
|
|
908
|
+
|
|
909
|
+
# Prepare request body
|
|
910
|
+
body_data = json.dumps(body).encode('utf-8')
|
|
911
|
+
|
|
912
|
+
try:
|
|
913
|
+
# Make the request
|
|
914
|
+
resp = pool_manager.request(
|
|
915
|
+
'POST',
|
|
916
|
+
getattr(configuration, 'host', 'https://api.catonetworks.com/api/v1/graphql'),
|
|
917
|
+
body=body_data,
|
|
918
|
+
headers=headers
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
# Parse response
|
|
922
|
+
if resp.status < 200 or resp.status >= 300:
|
|
923
|
+
reason = resp.reason if resp.reason is not None else "Unknown Error"
|
|
924
|
+
error_msg = f"HTTP {resp.status}: {reason}"
|
|
925
|
+
if resp.data:
|
|
926
|
+
try:
|
|
927
|
+
error_msg += f"\n{resp.data.decode('utf-8')}"
|
|
928
|
+
except Exception:
|
|
929
|
+
error_msg += f"\n{resp.data}"
|
|
930
|
+
print(f"ERROR: {error_msg}")
|
|
931
|
+
return None
|
|
932
|
+
|
|
933
|
+
try:
|
|
934
|
+
response_data = json.loads(resp.data.decode('utf-8'))
|
|
935
|
+
except json.JSONDecodeError:
|
|
936
|
+
response_data = resp.data.decode('utf-8')
|
|
937
|
+
|
|
938
|
+
# Return in the same format as the regular API client
|
|
939
|
+
return [response_data]
|
|
940
|
+
|
|
941
|
+
except Exception as e:
|
|
942
|
+
# Safely handle exception string conversion
|
|
943
|
+
try:
|
|
944
|
+
error_str = str(e)
|
|
945
|
+
except Exception:
|
|
946
|
+
error_str = f"Exception of type {type(e).__name__}"
|
|
947
|
+
print(f"ERROR: Network/request error: {error_str}")
|
|
948
|
+
return None
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
catocli/__init__.py,sha256=
|
|
1
|
+
catocli/__init__.py,sha256=HqBAr27AlpkoQiOSD0gHsZCdJJQ2kVVSjSyJkHAs1nI,85
|
|
2
2
|
catocli/__main__.py,sha256=6Z0ns_k_kUcz1Qtrn1u7UyUnqB-3e85jM_nppOwFsv4,217
|
|
3
|
-
catocli/Utils/clidriver.py,sha256=
|
|
3
|
+
catocli/Utils/clidriver.py,sha256=RHZ95heniZSCCHlkP8mNBe22CPdzR78fANxL5F6Q1q8,13991
|
|
4
4
|
catocli/Utils/profile_manager.py,sha256=Stch-cU2rLA7r07hETIRsvG_JxpQx3Q42QwpfD7Qd2I,6379
|
|
5
|
-
catocli/Utils/version_checker.py,sha256=
|
|
6
|
-
catocli/parsers/parserApiClient.py,sha256=
|
|
5
|
+
catocli/Utils/version_checker.py,sha256=tCtsCn7xxMIxOm6cWJSA_yPt0j4mNMK4iWSJej0yM6A,6696
|
|
6
|
+
catocli/parsers/parserApiClient.py,sha256=pSzh6IIXxPhytiLUG47DvC6bI3VU7xRczosyRrYoujs,35505
|
|
7
7
|
catocli/parsers/configure/__init__.py,sha256=Kq4OYGq_MXOQBHm8vxNkzJqCXp7zremYBfQ4Ai3_Lb4,3286
|
|
8
8
|
catocli/parsers/configure/configure.py,sha256=GyaOeuf0ZK3-wsZaczmO1OsKVJiPK04h6iI1u-EaKQc,12217
|
|
9
9
|
catocli/parsers/custom/README.md,sha256=UvCWAtF3Yh0UsvADb0ve1qJupgYHeyGu6V3Z0O5HEvo,8180
|
|
@@ -14,9 +14,10 @@ catocli/parsers/custom/export_rules/export_rules.py,sha256=gXig4NC29yC5nW48ri-j0
|
|
|
14
14
|
catocli/parsers/custom/export_sites/__init__.py,sha256=WnTRNA4z4IS0rKR9r_UfuGv3bxp8nlt4YNH1Sed4toU,1378
|
|
15
15
|
catocli/parsers/custom/export_sites/export_sites.py,sha256=DQxCZXGEN35F_kEbAAXo57t0-B-7koVRbe7unExo7F4,24670
|
|
16
16
|
catocli/parsers/custom/import_rules_to_tf/__init__.py,sha256=jFCFceFv8zDW7nGLZOXkFE6NXAMPYRwKQwTbhSawYdM,3908
|
|
17
|
-
catocli/parsers/custom/import_rules_to_tf/import_rules_to_tf.py,sha256=
|
|
17
|
+
catocli/parsers/custom/import_rules_to_tf/import_rules_to_tf.py,sha256=WlyTDxUtWchu9CDs73ILHDfNXCAh7jFARBEKk5WWhwM,18714
|
|
18
18
|
catocli/parsers/custom/import_sites_to_tf/__init__.py,sha256=0EyCGY2qV4wxMoFfqBPs5FoMc0Ufr7J7Ciygb6Xx8Fc,2925
|
|
19
|
-
catocli/parsers/custom/import_sites_to_tf/import_sites_to_tf.py,sha256=
|
|
19
|
+
catocli/parsers/custom/import_sites_to_tf/import_sites_to_tf.py,sha256=KND-eQOS_rFYVAX01Gm5ChxE8DtHZ1edS0SizoBqyx4,42880
|
|
20
|
+
catocli/parsers/custom_private/__init__.py,sha256=Xy5ODq-JZMBuIZ7HxBpbYlfn09p-NyxMU45TT6Tw9Sc,4170
|
|
20
21
|
catocli/parsers/mutation/README.md,sha256=mdOfOY0sOVGnf9q-GVgtGc7mtxFKigD9q0ILJAw8l0I,32
|
|
21
22
|
catocli/parsers/mutation_accountManagement/README.md,sha256=8ipbdTU8FAAfiH3IYlTZkTbLpPtO0fMblra7Zi8vwAI,249
|
|
22
23
|
catocli/parsers/mutation_accountManagement/__init__.py,sha256=8sDLa3dko18IePLDuskvkKNLYi0CtER-ZEFnAUaTGwg,6380
|
|
@@ -335,13 +336,13 @@ catocli/parsers/query_xdr_stories/README.md,sha256=kbnrCmTq7o0T1LMgN0w8lAoiwB3NZ
|
|
|
335
336
|
catocli/parsers/query_xdr_story/README.md,sha256=Csl2tE511miGOrISgIJyJXVEz2v1ONrMdvYywMeKv1Q,930
|
|
336
337
|
catocli/parsers/raw/README.md,sha256=qvDLcg7U12yqacIQStOPtkqTsTLltJ06SSVmbqvlZhY,739
|
|
337
338
|
catocli/parsers/raw/__init__.py,sha256=wXknxAeTYqjbOG_NFXsenjciD_bagT7P6Oxi2CuV8hg,1077
|
|
338
|
-
catocli-2.1.
|
|
339
|
+
catocli-2.1.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
339
340
|
graphql_client/__init__.py,sha256=2nxD4YsWoOnALXi5cXbmtIN_i0NL_eyDTQRTxs52mkI,315
|
|
340
341
|
graphql_client/api_client.py,sha256=2Rc1Zo1xH9Jnk1AO68kLSofTShkZwSVF-WkVtczfIc4,5786
|
|
341
342
|
graphql_client/api_client_types.py,sha256=dM3zl6FA5SSp6nR6KmLfTL1BKaXX9uPMCZAm4v_FiUs,11569
|
|
342
343
|
graphql_client/configuration.py,sha256=zzcdWjK1HdOxEbWg12ciWAlKV5sVpoiOzHiL5PMTBWU,7706
|
|
343
344
|
graphql_client/api/__init__.py,sha256=UjpnW5LhQScrcYDQhCfk5YkA4pGpEx0JT_G0gUI23Rg,88
|
|
344
|
-
graphql_client/api/call_api.py,sha256
|
|
345
|
+
graphql_client/api/call_api.py,sha256=K8IvubNIr6zsXPPiO7l126WnR-kCohyTZG9w4TPYBGw,3874
|
|
345
346
|
graphql_client/models/__init__.py,sha256=beY1dq6H1z0OOWRwvCN84LFOilZKMD_GjrYHuLKJI5c,275
|
|
346
347
|
graphql_client/models/no_schema.py,sha256=NorUxY0NQA6reSS8Os_iF0GRk6cCYxhH7_Su0B9_eJk,1972
|
|
347
348
|
models/mutation.accountManagement.addAccount.json,sha256=2gm59WwGde-8MVU2L4BIB5EpQ4ErBlE20qEaesTO5e8,55479
|
|
@@ -587,7 +588,7 @@ models/query.socketPortMetricsTimeSeries.json,sha256=61CJTpxNtWPS_7CnAQFW_rIe_ae
|
|
|
587
588
|
models/query.subDomains.json,sha256=ySJ-gsBzC8EX4WrtJpasLAGkSK7-3e3orhPbIyx_B8s,7041
|
|
588
589
|
models/query.xdr.stories.json,sha256=2VTpZjf47WbC4MYMoyvOxikkSUJRH2kwAmRLRAMluYk,3835486
|
|
589
590
|
models/query.xdr.story.json,sha256=3WOa45mNzQRXjy3LEQoRw9gHX29AUOISWhu7ohvdhwc,2838778
|
|
590
|
-
schema/catolib.py,sha256=
|
|
591
|
+
schema/catolib.py,sha256=sProS90AZV8tXuAXhc7H1ycO1tX7o4nvrpWU5ypZrsU,57845
|
|
591
592
|
schema/importSchema.py,sha256=9xg9N0MjgQUiPRczOpk0sTY1Nx9K2F6MRhpUyRTNqZ4,2795
|
|
592
593
|
schema/remove_policyid.py,sha256=DHGEL-Yb8mqNSPelDmmqYtHZi-tjZd2jwA4_ycgC9Rc,2590
|
|
593
594
|
schema/remove_policyid_mutations.py,sha256=TDNC5iMVqGtF_t9s9A4TkNCLMHAYmo77o3jKqjt8Uz8,2944
|
|
@@ -637,8 +638,8 @@ vendor/urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRp
|
|
|
637
638
|
vendor/urllib3/util/url.py,sha256=wHORhp80RAXyTlAIkTqLFzSrkU7J34ZDxX-tN65MBZk,15213
|
|
638
639
|
vendor/urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146
|
|
639
640
|
vendor/urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423
|
|
640
|
-
catocli-2.1.
|
|
641
|
-
catocli-2.1.
|
|
642
|
-
catocli-2.1.
|
|
643
|
-
catocli-2.1.
|
|
644
|
-
catocli-2.1.
|
|
641
|
+
catocli-2.1.1.dist-info/METADATA,sha256=-3qr5BfA-dEs1v2rJObAzEwpkdKayegIMCt7ymG1lYs,1286
|
|
642
|
+
catocli-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
643
|
+
catocli-2.1.1.dist-info/entry_points.txt,sha256=p4k9Orre6aWcqVrNmBbckmCs39h-1naMxRo2AjWmWZ4,50
|
|
644
|
+
catocli-2.1.1.dist-info/top_level.txt,sha256=F4qSgcjcW5wR9EFrO8Ud06F7ZQGFr04a9qALNQDyVxU,52
|
|
645
|
+
catocli-2.1.1.dist-info/RECORD,,
|
graphql_client/api/call_api.py
CHANGED
|
@@ -59,9 +59,27 @@ class CallApi(object):
|
|
|
59
59
|
header_params['x-api-key'] = self.api_client.configuration.api_key['x-api-key']
|
|
60
60
|
header_params['User-Agent'] = "Cato-CLI-v"+self.api_client.configuration.version
|
|
61
61
|
|
|
62
|
-
# Add custom headers from configuration
|
|
62
|
+
# Add custom headers from configuration with proper encoding handling
|
|
63
63
|
if using_custom_headers:
|
|
64
|
-
|
|
64
|
+
for key, value in self.api_client.configuration.custom_headers.items():
|
|
65
|
+
# Ensure header values can be encoded as latin-1 (HTTP header encoding)
|
|
66
|
+
try:
|
|
67
|
+
# Try to encode as latin-1 first (HTTP standard)
|
|
68
|
+
if isinstance(value, str):
|
|
69
|
+
value.encode('latin-1')
|
|
70
|
+
header_params[key] = value
|
|
71
|
+
except UnicodeEncodeError:
|
|
72
|
+
# If latin-1 encoding fails, encode as UTF-8 and then decode as latin-1
|
|
73
|
+
# This handles Unicode characters by converting them to percent-encoded format
|
|
74
|
+
if isinstance(value, str):
|
|
75
|
+
try:
|
|
76
|
+
# URL encode problematic Unicode characters
|
|
77
|
+
import urllib.parse
|
|
78
|
+
encoded_value = urllib.parse.quote(value, safe=':;=?@&')
|
|
79
|
+
header_params[key] = encoded_value
|
|
80
|
+
except Exception:
|
|
81
|
+
# As a last resort, replace problematic characters
|
|
82
|
+
header_params[key] = value.encode('ascii', 'replace').decode('ascii')
|
|
65
83
|
|
|
66
84
|
if args.get("v")==True:
|
|
67
85
|
print("Host: ",self.api_client.configuration.host)
|
schema/catolib.py
CHANGED
|
@@ -534,6 +534,7 @@ profile_manager = get_profile_manager()
|
|
|
534
534
|
CATO_DEBUG = bool(os.getenv("CATO_DEBUG", False))
|
|
535
535
|
from ..parsers.raw import raw_parse
|
|
536
536
|
from ..parsers.custom import custom_parse
|
|
537
|
+
from ..parsers.custom_private import private_parse
|
|
537
538
|
from ..parsers.query_siteLocation import query_siteLocation_parse
|
|
538
539
|
"""
|
|
539
540
|
for parserName in parsers:
|
|
@@ -563,6 +564,15 @@ def show_version_info(args, configuration=None):
|
|
|
563
564
|
print("Unable to check for updates (check your internet connection)")
|
|
564
565
|
return [{"success": True, "current_version": catocli.__version__, "latest_version": latest_version if not args.current_only else None}]
|
|
565
566
|
|
|
567
|
+
def load_private_settings():
|
|
568
|
+
# Load private settings from ~/.cato/settings.json
|
|
569
|
+
settings_file = os.path.expanduser("~/.cato/settings.json")
|
|
570
|
+
try:
|
|
571
|
+
with open(settings_file, 'r') as f:
|
|
572
|
+
return json.load(f)
|
|
573
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
574
|
+
return {}
|
|
575
|
+
|
|
566
576
|
def get_configuration(skip_api_key=False):
|
|
567
577
|
configuration = Configuration()
|
|
568
578
|
configuration.verify_ssl = False
|
|
@@ -585,10 +595,13 @@ def get_configuration(skip_api_key=False):
|
|
|
585
595
|
print(f"Run 'catocli configure set --profile {profile_name}' to update your credentials.")
|
|
586
596
|
exit(1)
|
|
587
597
|
|
|
598
|
+
# Use standard endpoint from profile for regular API calls
|
|
599
|
+
configuration.host = credentials['endpoint']
|
|
600
|
+
|
|
588
601
|
# Only set API key if not using custom headers file
|
|
602
|
+
# (Private settings are handled separately in createPrivateRequest)
|
|
589
603
|
if not skip_api_key:
|
|
590
604
|
configuration.api_key["x-api-key"] = credentials['cato_token']
|
|
591
|
-
configuration.host = credentials['endpoint']
|
|
592
605
|
configuration.accountID = credentials['account_id']
|
|
593
606
|
|
|
594
607
|
return configuration
|
|
@@ -613,6 +626,7 @@ version_parser.add_argument('--current-only', action='store_true', help='Show on
|
|
|
613
626
|
version_parser.set_defaults(func=show_version_info)
|
|
614
627
|
|
|
615
628
|
custom_parsers = custom_parse(subparsers)
|
|
629
|
+
private_parsers = private_parse(subparsers)
|
|
616
630
|
raw_parsers = subparsers.add_parser('raw', help='Raw GraphQL', usage=get_help("raw"))
|
|
617
631
|
raw_parser = raw_parse(raw_parsers)
|
|
618
632
|
query_parser = subparsers.add_parser('query', help='Query', usage='catocli query <operationName> [options]')
|
|
@@ -681,6 +695,7 @@ def main(args=None):
|
|
|
681
695
|
response = args.func(args, None)
|
|
682
696
|
else:
|
|
683
697
|
# Check if using headers file to determine if we should skip API key
|
|
698
|
+
# Note: Private settings should NOT affect regular API calls - only private commands
|
|
684
699
|
using_headers_file = hasattr(args, 'headers_file') and args.headers_file
|
|
685
700
|
|
|
686
701
|
# Get configuration from profiles
|
|
@@ -694,8 +709,8 @@ def main(args=None):
|
|
|
694
709
|
custom_headers.update(parse_headers_from_file(args.headers_file))
|
|
695
710
|
if custom_headers:
|
|
696
711
|
configuration.custom_headers.update(custom_headers)
|
|
697
|
-
# Handle account ID override
|
|
698
|
-
if args.func.__name__
|
|
712
|
+
# Handle account ID override (applies to all commands except raw)
|
|
713
|
+
if args.func.__name__ not in ["createRawRequest"]:
|
|
699
714
|
if hasattr(args, 'accountID') and args.accountID is not None:
|
|
700
715
|
# Command line override takes precedence
|
|
701
716
|
configuration.accountID = args.accountID
|
|
@@ -708,6 +723,9 @@ def main(args=None):
|
|
|
708
723
|
else:
|
|
709
724
|
if response!=None:
|
|
710
725
|
print(json.dumps(response[0], sort_keys=True, indent=4))
|
|
726
|
+
except KeyboardInterrupt:
|
|
727
|
+
print('Operation cancelled by user (Ctrl+C).')
|
|
728
|
+
exit(130) # Standard exit code for SIGINT
|
|
711
729
|
except Exception as e:
|
|
712
730
|
if isinstance(e, AttributeError):
|
|
713
731
|
print('Missing arguments. Usage: catocli <operation> -h')
|
|
@@ -717,7 +735,7 @@ def main(args=None):
|
|
|
717
735
|
else:
|
|
718
736
|
print('ERROR: ',e)
|
|
719
737
|
traceback.print_exc()
|
|
720
|
-
|
|
738
|
+
exit(1)
|
|
721
739
|
"""
|
|
722
740
|
writeFile("../catocli/Utils/clidriver.py",cliDriverStr)
|
|
723
741
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|