catocli 2.0.1__py3-none-any.whl → 2.0.2__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.

Files changed (104) hide show
  1. catocli/Utils/clidriver.py +37 -5
  2. catocli/__init__.py +1 -1
  3. catocli/parsers/custom/__init__.py +3 -2
  4. catocli/parsers/custom/customLib.py +252 -1
  5. catocli/parsers/custom/export_rules/__init__.py +5 -1
  6. catocli/parsers/custom/export_rules/export_rules.py +31 -182
  7. catocli/parsers/custom/export_sites/__init__.py +20 -0
  8. catocli/parsers/custom/export_sites/export_sites.py +233 -0
  9. catocli/parsers/custom/import_rules_to_tf/__init__.py +2 -2
  10. catocli/parsers/custom/import_rules_to_tf/import_rules_to_tf.py +19 -9
  11. catocli/parsers/mutation_accountManagement/__init__.py +15 -18
  12. catocli/parsers/mutation_admin/__init__.py +15 -18
  13. catocli/parsers/mutation_container/__init__.py +5 -6
  14. catocli/parsers/mutation_hardware/__init__.py +5 -6
  15. catocli/parsers/mutation_policy/__init__.py +582 -504
  16. catocli/parsers/mutation_policy_terminalServer/README.md +7 -0
  17. catocli/parsers/mutation_policy_terminalServer_addRule/README.md +18 -0
  18. catocli/parsers/mutation_policy_terminalServer_addSection/README.md +18 -0
  19. catocli/parsers/mutation_policy_terminalServer_createPolicyRevision/README.md +18 -0
  20. catocli/parsers/mutation_policy_terminalServer_discardPolicyRevision/README.md +18 -0
  21. catocli/parsers/mutation_policy_terminalServer_moveRule/README.md +18 -0
  22. catocli/parsers/mutation_policy_terminalServer_moveSection/README.md +18 -0
  23. catocli/parsers/mutation_policy_terminalServer_publishPolicyRevision/README.md +18 -0
  24. catocli/parsers/mutation_policy_terminalServer_removeRule/README.md +18 -0
  25. catocli/parsers/mutation_policy_terminalServer_removeSection/README.md +18 -0
  26. catocli/parsers/mutation_policy_terminalServer_updatePolicy/README.md +18 -0
  27. catocli/parsers/mutation_policy_terminalServer_updateRule/README.md +18 -0
  28. catocli/parsers/mutation_policy_terminalServer_updateSection/README.md +18 -0
  29. catocli/parsers/mutation_sandbox/__init__.py +10 -12
  30. catocli/parsers/mutation_site/__init__.py +164 -150
  31. catocli/parsers/mutation_site_addSocketAddOnCard/README.md +17 -0
  32. catocli/parsers/mutation_site_removeSocketAddOnCard/README.md +17 -0
  33. catocli/parsers/mutation_site_startSiteUpgrade/README.md +17 -0
  34. catocli/parsers/mutation_sites/__init__.py +164 -150
  35. catocli/parsers/mutation_sites_addSocketAddOnCard/README.md +17 -0
  36. catocli/parsers/mutation_sites_removeSocketAddOnCard/README.md +17 -0
  37. catocli/parsers/mutation_sites_startSiteUpgrade/README.md +17 -0
  38. catocli/parsers/mutation_xdr/__init__.py +15 -18
  39. catocli/parsers/query_accountBySubdomain/__init__.py +5 -6
  40. catocli/parsers/query_accountManagement/__init__.py +5 -6
  41. catocli/parsers/query_accountMetrics/__init__.py +5 -6
  42. catocli/parsers/query_accountRoles/__init__.py +5 -6
  43. catocli/parsers/query_accountSnapshot/__init__.py +5 -6
  44. catocli/parsers/query_admin/__init__.py +5 -6
  45. catocli/parsers/query_admins/__init__.py +5 -6
  46. catocli/parsers/query_appStats/__init__.py +5 -6
  47. catocli/parsers/query_appStatsTimeSeries/__init__.py +5 -6
  48. catocli/parsers/query_auditFeed/__init__.py +5 -6
  49. catocli/parsers/query_catalogs/__init__.py +5 -6
  50. catocli/parsers/query_container/__init__.py +5 -6
  51. catocli/parsers/query_devices/README.md +2 -1
  52. catocli/parsers/query_devices/__init__.py +5 -6
  53. catocli/parsers/query_entityLookup/__init__.py +5 -6
  54. catocli/parsers/query_events/__init__.py +5 -6
  55. catocli/parsers/query_eventsFeed/README.md +1 -1
  56. catocli/parsers/query_eventsFeed/__init__.py +5 -6
  57. catocli/parsers/query_eventsTimeSeries/__init__.py +5 -6
  58. catocli/parsers/query_hardware/__init__.py +5 -6
  59. catocli/parsers/query_hardwareManagement/__init__.py +5 -6
  60. catocli/parsers/query_licensing/__init__.py +5 -6
  61. catocli/parsers/query_policy/README.md +2 -1
  62. catocli/parsers/query_policy/__init__.py +5 -6
  63. catocli/parsers/query_sandbox/__init__.py +5 -6
  64. catocli/parsers/query_site/README.md +2 -1
  65. catocli/parsers/query_site/__init__.py +5 -6
  66. catocli/parsers/query_siteLocation/__init__.py +3 -7
  67. catocli/parsers/query_subDomains/__init__.py +5 -6
  68. catocli/parsers/query_xdr/__init__.py +10 -12
  69. catocli/parsers/raw/__init__.py +2 -0
  70. {catocli-2.0.1.dist-info → catocli-2.0.2.dist-info}/METADATA +1 -1
  71. {catocli-2.0.1.dist-info → catocli-2.0.2.dist-info}/RECORD +104 -65
  72. graphql_client/api/call_api.py +12 -6
  73. models/mutation.policy.remotePortFwd.updateRule.json +6 -6
  74. models/mutation.policy.terminalServer.addRule.json +2403 -0
  75. models/mutation.policy.terminalServer.addSection.json +1358 -0
  76. models/mutation.policy.terminalServer.createPolicyRevision.json +1873 -0
  77. models/mutation.policy.terminalServer.discardPolicyRevision.json +1807 -0
  78. models/mutation.policy.terminalServer.moveRule.json +1605 -0
  79. models/mutation.policy.terminalServer.moveSection.json +1259 -0
  80. models/mutation.policy.terminalServer.publishPolicyRevision.json +1864 -0
  81. models/mutation.policy.terminalServer.removeRule.json +1253 -0
  82. models/mutation.policy.terminalServer.removeSection.json +958 -0
  83. models/mutation.policy.terminalServer.updatePolicy.json +1883 -0
  84. models/mutation.policy.terminalServer.updateRule.json +2096 -0
  85. models/mutation.policy.terminalServer.updateSection.json +1111 -0
  86. models/mutation.site.addSocketAddOnCard.json +1050 -0
  87. models/mutation.site.removeSocketAddOnCard.json +786 -0
  88. models/mutation.site.startSiteUpgrade.json +802 -0
  89. models/mutation.sites.addSocketAddOnCard.json +1050 -0
  90. models/mutation.sites.removeSocketAddOnCard.json +786 -0
  91. models/mutation.sites.startSiteUpgrade.json +802 -0
  92. models/query.devices.json +311 -2
  93. models/query.events.json +48 -0
  94. models/query.eventsFeed.json +12 -0
  95. models/query.eventsTimeSeries.json +36 -0
  96. models/query.licensing.json +21815 -10093
  97. models/query.policy.json +1898 -305
  98. models/query.site.json +225 -0
  99. models/query.siteLocation.json +97190 -295396
  100. schema/catolib.py +52 -24
  101. {catocli-2.0.1.dist-info → catocli-2.0.2.dist-info}/LICENSE +0 -0
  102. {catocli-2.0.1.dist-info → catocli-2.0.2.dist-info}/WHEEL +0 -0
  103. {catocli-2.0.1.dist-info → catocli-2.0.2.dist-info}/entry_points.txt +0 -0
  104. {catocli-2.0.1.dist-info → catocli-2.0.2.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@ import json
3
3
  import sys
4
4
  from graphql_client.api.call_api import ApiClient, CallApi
5
5
  from graphql_client.api_client import ApiException
6
-
6
+ from ..customLib import writeDataToFile, makeCall, getAccountID
7
7
 
8
8
  def strip_ids_recursive(data):
9
9
  """Recursively strip id attributes from data structure, but only from objects that contain only 'id' and 'name' keys"""
@@ -37,40 +37,7 @@ def export_if_rules_to_json(args, configuration):
37
37
  Adapted from scripts/export_if_rules_to_json.py
38
38
  """
39
39
  try:
40
- # Get account ID from args, configuration, or environment variable
41
- account_id = None
42
- if hasattr(args, 'accountID') and args.accountID:
43
- account_id = args.accountID
44
- elif hasattr(configuration, 'accountID') and configuration.accountID:
45
- account_id = configuration.accountID
46
- else:
47
- account_id = os.getenv('CATO_ACCOUNT_ID')
48
-
49
- if not account_id:
50
- raise ValueError("Account ID is required. Provide it using the -accountID flag or set CATO_ACCOUNT_ID environment variable.")
51
-
52
- # Set up output file path
53
- if hasattr(args, 'output_file_path') and args.output_file_path:
54
- # Use output file path if provided
55
- output_file = args.output_file_path
56
- destination_dir = os.path.dirname(output_file)
57
- if hasattr(args, 'verbose') and args.verbose:
58
- print(f"Using output file path: {output_file}")
59
- else:
60
- # Use default path and filename
61
- destination_dir = 'config_data'
62
- json_output_file = f"all_ifw_rules_and_sections_{account_id}.json"
63
- output_file = os.path.join(destination_dir, json_output_file)
64
- if hasattr(args, 'verbose') and args.verbose:
65
- print(f"Using default path: {output_file}")
66
-
67
- # Create destination directory if it doesn't exist
68
- if destination_dir and not os.path.exists(destination_dir):
69
- if hasattr(args, 'verbose') and args.verbose:
70
- print(f"Creating directory: {destination_dir}")
71
- os.makedirs(destination_dir)
72
-
73
- # Define the GraphQL query
40
+ account_id = getAccountID(args, configuration)
74
41
  policy_query = {
75
42
  "query": "query policy ( $accountId:ID! ) { policy ( accountId:$accountId ) { internetFirewall { policy { enabled rules { audit { updatedTime updatedBy } rule { id name description index section { id name } enabled source { ip host { id name } site { id name } subnet ipRange { from to } globalIpRange { id name } networkInterface { id name } siteNetworkSubnet { id name } floatingSubnet { id name } user { id name } usersGroup { id name } group { id name } systemGroup { id name } } connectionOrigin country { id name } device { id name } deviceOS deviceAttributes { category type model manufacturer os osVersion } destination { application { id name } customApp { id name } appCategory { id name } customCategory { id name } sanctionedAppsCategory { id name } country { id name } domain fqdn ip subnet ipRange { from to } globalIpRange { id name } remoteAsn containers { fqdnContainer { id name } ipAddressRangeContainer { id name } } } service { standard { id name } custom { port portRange { from to } protocol } } action tracking { event { enabled } alert { enabled frequency subscriptionGroup { id name } webhook { id name } mailingList { id name } } } schedule { activeOn customTimeframePolicySchedule: customTimeframe { from to } customRecurringPolicySchedule: customRecurring { from to days } } exceptions { name source { ip host { id name } site { id name } subnet ipRange { from to } globalIpRange { id name } networkInterface { id name } siteNetworkSubnet { id name } floatingSubnet { id name } user { id name } usersGroup { id name } group { id name } systemGroup { id name } } deviceOS country { id name } device { id name } deviceAttributes { category type model manufacturer os osVersion } destination { application { id name } customApp { id name } appCategory { id name } customCategory { id name } sanctionedAppsCategory { id name } country { id name } domain fqdn ip subnet ipRange { from to } globalIpRange { id name } remoteAsn containers { fqdnContainer { id name } ipAddressRangeContainer { id name } } } service { standard { id name } custom { port portRangeCustomService: portRange { from to } protocol } } connectionOrigin } } properties } sections { audit { updatedTime updatedBy } section { id name } properties } audit { publishedTime publishedBy } revision { id name description changes createdTime updatedTime } } } } }",
76
43
  "variables": {
@@ -78,56 +45,7 @@ def export_if_rules_to_json(args, configuration):
78
45
  },
79
46
  "operationName": "policy"
80
47
  }
81
-
82
- # Create API client instance with params
83
- instance = CallApi(ApiClient(configuration))
84
- params = {
85
- 'v': hasattr(args, 'verbose') and args.verbose, # verbose mode
86
- 'f': 'json', # format
87
- 'p': False, # pretty print
88
- 't': False # test mode
89
- }
90
-
91
- try:
92
- # Call the API directly
93
- # NOTE: The API client (graphql_client/api_client_types.py lines 106-108)
94
- # automatically prints error responses and exits on GraphQL errors.
95
- # This means our custom error handling below may not be reached if there are GraphQL errors.
96
- response = instance.call_api(policy_query, params)
97
- all_ifw_rules = response[0] if response else {}
98
-
99
- # Show raw API response in verbose mode
100
- if hasattr(args, 'verbose') and args.verbose:
101
- print("\n" + "=" * 80)
102
- print("RAW API RESPONSE:")
103
- print("=" * 80)
104
- print(json.dumps(all_ifw_rules, indent=2))
105
- print("=" * 80 + "\n")
106
-
107
- # Check for GraphQL errors first (may not be reached due to API client behavior)
108
- if 'errors' in all_ifw_rules:
109
- error_messages = [error.get('message', 'Unknown error') for error in all_ifw_rules['errors']]
110
- raise Exception(f"API returned errors: {', '.join(error_messages)}")
111
-
112
- if not all_ifw_rules or 'data' not in all_ifw_rules:
113
- raise ValueError("Failed to retrieve data from API")
114
-
115
- except ApiException as e:
116
- raise Exception(f"API call failed - {e}")
117
- except Exception as e:
118
- raise Exception(f"Unexpected error during API call - {e}")
119
-
120
- # IMPORTANT: Preserve section IDs BEFORE stripping them
121
- section_id_map = {}
122
- section_to_start_after_id = None
123
- sections_with_ids = all_ifw_rules['data']['policy']['internetFirewall']['policy']['sections']
124
- for index, section_data in enumerate(sections_with_ids):
125
- section_name = section_data['section']['name']
126
- section_id = section_data['section']['id']
127
- if index == 0:
128
- section_to_start_after_id = section_id
129
- else:
130
- section_id_map[section_name] = section_id
48
+ all_ifw_rules = makeCall(args, configuration, policy_query)
131
49
 
132
50
  # Processing data to strip id attributes
133
51
  processed_data = strip_ids_recursive(all_ifw_rules)
@@ -173,26 +91,28 @@ def export_if_rules_to_json(args, configuration):
173
91
 
174
92
  # Reformat sections array to have index, section_id and section_name structure
175
93
  # Exclude the first section from export
94
+ sections_with_ids = all_ifw_rules['data']['policy']['internetFirewall']['policy']['sections']
176
95
  processed_sections = []
177
- for index, section_data in enumerate(processed_data['data']['policy']['internetFirewall']['policy']['sections']):
96
+ for index, section_data in enumerate(sections_with_ids):
97
+ # print("sections_with_ids",json.dumps(section_data, indent=2))
178
98
  if index > 0: # Skip the first section (index 0)
179
99
  processed_sections.append({
180
100
  "section_index": index,
181
- "section_name": section_data['section']['name']
101
+ "section_name": section_data['section']['name'],
102
+ "section_id": section_data['section']['id']
182
103
  })
183
-
184
- # Add preserved section IDs and section_to_start_after_id
185
- processed_data['data']['policy']['internetFirewall']['policy']['section_ids'] = section_id_map
186
- if section_to_start_after_id:
187
- processed_data['data']['policy']['internetFirewall']['policy']['section_to_start_after_id'] = section_to_start_after_id
188
104
 
189
105
  # Replace the original sections array with the reformatted one
190
106
  processed_data['data']['policy']['internetFirewall']['policy']['sections'] = processed_sections
191
107
 
192
- # Write the processed data to the new file
193
- with open(output_file, 'w', encoding='utf-8') as f:
194
- json.dump(processed_data, f, indent=4, ensure_ascii=False)
195
-
108
+ output_file = writeDataToFile(
109
+ data=processed_data,
110
+ args=args,
111
+ account_id=account_id,
112
+ default_filename_template="all_ifw_rules_and_sections_{account_id}.json",
113
+ default_directory="config_data"
114
+ )
115
+
196
116
  return [{"success": True, "output_file": output_file, "account_id": account_id}]
197
117
 
198
118
  except Exception as e:
@@ -206,40 +126,8 @@ def export_wf_rules_to_json(args, configuration):
206
126
  Adapted from scripts/export_wf_rules_to_json.py
207
127
  """
208
128
  try:
209
- # Get account ID from args, configuration, or environment variable
210
- account_id = None
211
- if hasattr(args, 'accountID') and args.accountID:
212
- account_id = args.accountID
213
- elif hasattr(configuration, 'accountID') and configuration.accountID:
214
- account_id = configuration.accountID
215
- else:
216
- account_id = os.getenv('CATO_ACCOUNT_ID')
217
-
218
- if not account_id:
219
- raise ValueError("Account ID is required. Provide it using the -accountID flag or set CATO_ACCOUNT_ID environment variable.")
129
+ account_id = getAccountID(args, configuration)
220
130
 
221
- # Set up output file path
222
- if hasattr(args, 'output_file_path') and args.output_file_path:
223
- # Use output file path if provided
224
- output_file = args.output_file_path
225
- destination_dir = os.path.dirname(output_file)
226
- if hasattr(args, 'verbose') and args.verbose:
227
- print(f"Using output file path: {output_file}")
228
- else:
229
- # Use default path and filename
230
- destination_dir = 'config_data'
231
- json_output_file = f"all_wf_rules_and_sections_{account_id}.json"
232
- output_file = os.path.join(destination_dir, json_output_file)
233
- if hasattr(args, 'verbose') and args.verbose:
234
- print(f"Using default path: {output_file}")
235
-
236
- # Create destination directory if it doesn't exist
237
- if destination_dir and not os.path.exists(destination_dir):
238
- if hasattr(args, 'verbose') and args.verbose:
239
- print(f"Creating directory: {destination_dir}")
240
- os.makedirs(destination_dir)
241
-
242
- # Define the GraphQL query for WAN Firewall
243
131
  policy_query = {
244
132
  "query": "query policy ( $accountId:ID! ) { policy ( accountId:$accountId ) { wanFirewall { policy { enabled rules { audit { updatedTime updatedBy } rule { id name description index section { id name } enabled source { host { id name } site { id name } subnet ip ipRange { from to } globalIpRange { id name } networkInterface { id name } siteNetworkSubnet { id name } floatingSubnet { id name } user { id name } usersGroup { id name } group { id name } systemGroup { id name } } connectionOrigin country { id name } device { id name } deviceOS deviceAttributes { category type model manufacturer os osVersion } destination { host { id name } site { id name } subnet ip ipRange { from to } globalIpRange { id name } networkInterface { id name } siteNetworkSubnet { id name } floatingSubnet { id name } user { id name } usersGroup { id name } group { id name } systemGroup { id name } } application { application { id name } appCategory { id name } customApp { id name } customCategory { id name } sanctionedAppsCategory { id name } domain fqdn ip subnet ipRange { from to } globalIpRange { id name } } service { standard { id name } custom { port portRange { from to } protocol } } action tracking { event { enabled } alert { enabled frequency subscriptionGroup { id name } webhook { id name } mailingList { id name } } } schedule { activeOn customTimeframePolicySchedule: customTimeframe { from to } customRecurringPolicySchedule: customRecurring { from to days } } direction exceptions { name source { host { id name } site { id name } subnet ip ipRange { from to } globalIpRange { id name } networkInterface { id name } siteNetworkSubnet { id name } floatingSubnet { id name } user { id name } usersGroup { id name } group { id name } systemGroup { id name } } deviceOS destination { host { id name } site { id name } subnet ip ipRange { from to } globalIpRange { id name } networkInterface { id name } siteNetworkSubnet { id name } floatingSubnet { id name } user { id name } usersGroup { id name } group { id name } systemGroup { id name } } country { id name } device { id name } deviceAttributes { category type model manufacturer os osVersion } application { application { id name } appCategory { id name } customApp { id name } customCategory { id name } sanctionedAppsCategory { id name } domain fqdn ip subnet ipRange { from to } globalIpRange { id name } } service { standard { id name } custom { port portRangeCustomService: portRange { from to } protocol } } connectionOrigin direction } } properties } sections { audit { updatedTime updatedBy } section { id name } properties } audit { publishedTime publishedBy } revision { id name description changes createdTime updatedTime } } } } }",
245
133
  "variables": {
@@ -247,51 +135,11 @@ def export_wf_rules_to_json(args, configuration):
247
135
  },
248
136
  "operationName": "policy"
249
137
  }
250
-
251
- # Create API client instance with params
252
- instance = CallApi(ApiClient(configuration))
253
- params = {
254
- 'v': hasattr(args, 'verbose') and args.verbose, # verbose mode
255
- 'f': 'json', # format
256
- 'p': False, # pretty print
257
- 't': False # test mode
258
- }
259
-
260
- try:
261
- # Call the API directly
262
- response = instance.call_api(policy_query, params)
263
- all_wf_rules = response[0] if response else {}
264
-
265
- # Show raw API response in verbose mode
266
- if hasattr(args, 'verbose') and args.verbose:
267
- print("\n" + "=" * 80)
268
- print("RAW API RESPONSE:")
269
- print("=" * 80)
270
- print(json.dumps(all_wf_rules, indent=2))
271
- print("=" * 80 + "\n")
272
-
273
- # Check for GraphQL errors first
274
- if 'errors' in all_wf_rules:
275
- error_messages = [error.get('message', 'Unknown error') for error in all_wf_rules['errors']]
276
- raise Exception(f"API returned errors: {', '.join(error_messages)}")
277
-
278
- if not all_wf_rules or 'data' not in all_wf_rules:
279
- raise ValueError("Failed to retrieve data from API")
138
+ all_wf_rules = makeCall(args, configuration, policy_query)
280
139
 
281
- except ApiException as e:
282
- raise Exception(f"API call failed - {e}")
283
- except Exception as e:
284
- raise Exception(f"Unexpected error during API call - {e}")
140
+ if not all_wf_rules or 'data' not in all_wf_rules:
141
+ raise ValueError("Failed to retrieve data from API")
285
142
 
286
- # IMPORTANT: Preserve section IDs BEFORE stripping them
287
- section_id_map = {}
288
- section_to_start_after_id = None
289
- sections_with_ids = all_wf_rules['data']['policy']['wanFirewall']['policy']['sections']
290
- for index, section_data in enumerate(sections_with_ids):
291
- section_name = section_data['section']['name']
292
- section_id = section_data['section']['id']
293
- section_id_map[section_name] = section_id
294
-
295
143
  # Processing data to strip id attributes
296
144
  processed_data = strip_ids_recursive(all_wf_rules)
297
145
 
@@ -335,24 +183,25 @@ def export_wf_rules_to_json(args, configuration):
335
183
 
336
184
  # Reformat sections array to have index, section_id and section_name structure
337
185
  # Exclude the first section from export
186
+ sections_with_ids = all_wf_rules['data']['policy']['wanFirewall']['policy']['sections']
338
187
  processed_sections = []
339
- for index, section_data in enumerate(processed_data['data']['policy']['wanFirewall']['policy']['sections']):
188
+ for index, section_data in enumerate(sections_with_ids):
340
189
  processed_sections.append({
341
190
  "section_index": index,
342
- "section_name": section_data['section']['name']
191
+ "section_name": section_data['section']['name'],
192
+ "section_id": section_data['section']['id']
343
193
  })
344
-
345
- # Add preserved section IDs and section_to_start_after_id
346
- processed_data['data']['policy']['wanFirewall']['policy']['section_ids'] = section_id_map
347
- if section_to_start_after_id:
348
- processed_data['data']['policy']['wanFirewall']['policy']['section_to_start_after_id'] = section_to_start_after_id
349
194
 
350
195
  # Replace the original sections array with the reformatted one
351
196
  processed_data['data']['policy']['wanFirewall']['policy']['sections'] = processed_sections
352
197
 
353
- # Write the processed data to the new file
354
- with open(output_file, 'w', encoding='utf-8') as f:
355
- json.dump(processed_data, f, indent=4, ensure_ascii=False)
198
+ output_file = writeDataToFile(
199
+ data=processed_data,
200
+ args=args,
201
+ account_id=account_id,
202
+ default_filename_template="all_wf_rules_and_sections_{account_id}.json",
203
+ default_directory="config_data"
204
+ )
356
205
 
357
206
  return [{"success": True, "output_file": output_file, "account_id": account_id}]
358
207
 
@@ -0,0 +1,20 @@
1
+ import catocli.parsers.custom.export_sites.export_sites as export_sites
2
+
3
+ def export_sites_parse(subparsers):
4
+ """Create export_sites command parsers"""
5
+
6
+ # Create the socket_sites parser (direct command, no subparsers)
7
+ socket_sites_parser = subparsers.add_parser(
8
+ 'socket_sites',
9
+ help='Export consolidated site and socket data to JSON format',
10
+ usage='catocli export socket_sites [-accountID <account_id>] [options]'
11
+ )
12
+
13
+ socket_sites_parser.add_argument('-accountID', help='Account ID to export data from (uses CATO_ACCOUNT_ID environment variable if not specified)', required=False)
14
+ socket_sites_parser.add_argument('-siteIDs', help='Comma-separated list of site IDs to export (e.g., "132606,132964,133511")', required=False)
15
+ socket_sites_parser.add_argument('--output-file-path', help='Full path including filename and extension for output file. If not specified, uses default: config_data/socket_site_data_{account_id}.json')
16
+ socket_sites_parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
17
+
18
+ socket_sites_parser.set_defaults(func=export_sites.export_socket_site_to_json)
19
+
20
+ return socket_sites_parser
@@ -0,0 +1,233 @@
1
+ import os
2
+ import json
3
+ from graphql_client.api.call_api import ApiClient, CallApi
4
+ from graphql_client.api_client import ApiException
5
+ from ..customLib import writeDataToFile, makeCall, getAccountID
6
+
7
+ def export_socket_site_to_json(args, configuration):
8
+ """
9
+ Export consolidated site and socket data to JSON format
10
+ """
11
+ processed_data = {'sites':{}}
12
+
13
+ try:
14
+ account_id = getAccountID(args, configuration)
15
+ # Get account snapshot with siteIDs if provided
16
+ # Get siteIDs from args if provided (comma-separated string)
17
+ site_ids = []
18
+ if hasattr(args, 'siteIDs') and args.siteIDs:
19
+ # Parse comma-separated string into list, removing whitespace
20
+ site_ids = [site_id.strip() for site_id in args.siteIDs.split(',') if site_id.strip()]
21
+ if hasattr(args, 'verbose') and args.verbose:
22
+ print(f"Filtering snapshot for site IDs: {site_ids}")
23
+
24
+ ###############################################################
25
+ ## Call APIs to retrieve sites, interface and network ranges ##
26
+ ###############################################################
27
+ snapshot_sites = getAccountSnapshot(args, configuration, account_id, site_ids)
28
+ entity_network_interfaces = getEntityLookup(args, configuration, account_id, "networkInterface")
29
+ entity_network_ranges = getEntityLookup(args, configuration, account_id, "siteRange")
30
+ entity_sites = getEntityLookup(args, configuration, account_id, "site")
31
+
32
+ ##################################################################
33
+ ## Create processed_data object indexed by siteId with location ##
34
+ ##################################################################
35
+ for site_data in snapshot_sites:
36
+ cur_site = {
37
+ 'wan_interfaces': {},
38
+ 'lan_interfaces': {},
39
+ }
40
+ site_id = site_data.get('id')
41
+ cur_site['id'] = site_id
42
+ cur_site['name'] = site_data.get('infoSiteSnapshot', {}).get('name')
43
+ cur_site['description'] = site_data.get('infoSiteSnapshot', {}).get('description')
44
+ cur_site['connectionType'] = site_data.get('infoSiteSnapshot', {}).get('connType')
45
+ cur_site['type'] = site_data.get('infoSiteSnapshot', {}).get('type')
46
+ cur_site = populateSiteLocationData(args, site_data, cur_site)
47
+
48
+ site_interfaces = site_data.get('infoSiteSnapshot', {}).get('interfaces', [])
49
+ for wan_ni in site_interfaces:
50
+ cur_wan_interface = {}
51
+ id = wan_ni.get('wanRoleInterfaceInfo', "")
52
+ if id[0:3] == "wan":
53
+ cur_wan_interface['id'] = id
54
+ cur_wan_interface['name'] = wan_ni.get('name', "")
55
+ cur_wan_interface['upstreamBandwidth'] = wan_ni.get('upstreamBandwidth', 0)
56
+ cur_wan_interface['downstreamBandwidth'] = wan_ni.get('downstreamBandwidth', 0)
57
+ cur_wan_interface['destType'] = wan_ni.get('destType', "")
58
+ cur_site['wan_interfaces'][id] = cur_wan_interface
59
+
60
+ if site_id:
61
+ processed_data['sites'][site_id] = cur_site
62
+
63
+ ##################################################################################
64
+ ## Process entity lookup LAN network interfaces adding to site object by site_id##
65
+ ##################################################################################
66
+ for lan_ni in entity_network_interfaces:
67
+ cur_lan_interface = {
68
+ 'network_ranges': {},
69
+ }
70
+ site_id = lan_ni.get("helperFields","").get('siteId', "")
71
+ id = lan_ni.get('entity', "").get('id', "")
72
+ interfaceName = lan_ni.get('entity', "").get('interfaceName', "")
73
+ cur_lan_interface['id'] = id
74
+ cur_lan_interface['name'] = interfaceName
75
+ cur_lan_interface['index'] = lan_ni.get("helperFields","").get('interfaceId', "")
76
+ cur_lan_interface['upstreamBandwidth'] = lan_ni.get('upstreamBandwidth', 0)
77
+ cur_lan_interface['downstreamBandwidth'] = lan_ni.get('downstreamBandwidth', 0)
78
+ cur_lan_interface['destType'] = lan_ni.get('destType', "")
79
+ processed_data['sites'][site_id]['lan_interfaces'][interfaceName] = cur_lan_interface
80
+
81
+ #############################################################################
82
+ ## Process entity lookup network ranges populating by network interface id ##
83
+ #############################################################################
84
+ for range in entity_network_ranges:
85
+ cur_range = {}
86
+ site_id = lan_ni.get("helperFields","").get('siteId', "")
87
+ id = lan_ni.get('entity', "").get('id', "")
88
+ interface_name = lan_ni.get('entity', "").get('interfaceName', "")
89
+ cur_lan_interface['id'] = id
90
+ cur_lan_interface['subnet'] = lan_ni.get("helperFields","").get('subnet', "")
91
+ cur_lan_interface['vlanTag'] = lan_ni.get("helperFields","").get('vlanTag', "")
92
+ cur_lan_interface['microsegmentation'] = lan_ni.get("helperFields","").get('microsegmentation', "")
93
+
94
+ processed_data['sites'][site_id]['lan_interfaces'][interface_name] = cur_range
95
+
96
+
97
+ # Write the processed data to file using the general-purpose function
98
+ output_file = writeDataToFile(
99
+ data=processed_data,
100
+ args=args,
101
+ account_id=account_id,
102
+ default_filename_template="socket_sites_{account_id}.json",
103
+ default_directory="config_data"
104
+ )
105
+
106
+ return [{"success": True, "output_file": output_file, "account_id": account_id}]
107
+
108
+ except Exception as e:
109
+ print(f"ERROR: {str(e)}")
110
+ return [{"success": False, "error": str(e)}]
111
+
112
+
113
+ ##########################################################################
114
+ ########################### Helper functions #############################
115
+ ##########################################################################
116
+
117
+ def populateSiteLocationData(args, site_data, cur_site):
118
+ # Load site location data for timezone and state code lookups
119
+ site_location_data = {}
120
+ try:
121
+ script_dir = os.path.dirname(os.path.abspath(__file__))
122
+ models_dir = os.path.join(script_dir, '..', '..', '..', '..', 'models')
123
+ location_file = os.path.join(models_dir, 'query.siteLocation.json')
124
+
125
+ if os.path.exists(location_file):
126
+ with open(location_file, 'r', encoding='utf-8') as f:
127
+ site_location_data = json.load(f)
128
+ if hasattr(args, 'verbose') and args.verbose:
129
+ print(f"Loaded {len(site_location_data)} location entries from {location_file}")
130
+ else:
131
+ if hasattr(args, 'verbose') and args.verbose:
132
+ print(f"Warning: Site location file not found at {location_file}")
133
+ except Exception as e:
134
+ if hasattr(args, 'verbose') and args.verbose:
135
+ print(f"Warning: Could not load site location data: {e}")
136
+
137
+ ## siteLocation attributes
138
+ cur_site['address'] = site_data.get('infoSiteSnapshot', {}).get('address')
139
+ cur_site['city'] = site_data.get('infoSiteSnapshot', {}).get('cityName')
140
+ cur_site['stateName'] = site_data.get('infoSiteSnapshot', {}).get('countryStateName')
141
+ cur_site['countryCode'] = site_data.get('infoSiteSnapshot', {}).get('countryCode')
142
+ cur_site['countryName'] = site_data.get('infoSiteSnapshot', {}).get('countryName')
143
+
144
+ # Look up timezone and state code from location data
145
+ country_name = cur_site['countryName']
146
+ state_name = cur_site['stateName']
147
+ city = cur_site['city']
148
+
149
+ # Create lookup key based on available data
150
+ if state_name:
151
+ lookup_key = f"{country_name}___{state_name}___{city}"
152
+ else:
153
+ lookup_key = f"{country_name}___{city}"
154
+
155
+ # Debug output for lookup
156
+ if hasattr(args, 'verbose') and args.verbose:
157
+ print(f"Site {cur_site['name']}: Looking up '{lookup_key}'")
158
+
159
+ # Look up location details
160
+ location_data = site_location_data.get(lookup_key, {})
161
+
162
+ if hasattr(args, 'verbose') and args.verbose:
163
+ if location_data:
164
+ print(f" Found location data: {location_data}")
165
+ else:
166
+ print(f" No location data found for key: {lookup_key}")
167
+ # Try to find similar keys for debugging
168
+ similar_keys = [k for k in site_location_data.keys() if country_name in k and (not city or city in k)][:5]
169
+ if similar_keys:
170
+ print(f" Similar keys found: {similar_keys}")
171
+
172
+ cur_site['stateCode'] = location_data.get('stateCode', None)
173
+
174
+ # Get timezone - always use the 0 element in the timezones array
175
+ timezones = location_data.get('timezone', [])
176
+ cur_site['timezone'] = timezones[0] if timezones else None
177
+ return cur_site
178
+
179
+ def getEntityLookup(args, configuration, account_id, entity_type):
180
+ """
181
+ Helper function to get entity lookup data for a specific entity type
182
+ """
183
+ #################################
184
+ ## Get entity lookup for sites ##
185
+ #################################
186
+ entity_query = {
187
+ "query": "query entityLookup ( $accountID:ID! $type:EntityType! $sortInput:[SortInput] $lookupFilterInput:[LookupFilterInput] ) { entityLookup ( accountID:$accountID type:$type sort:$sortInput filters:$lookupFilterInput ) { items { entity { id name type } description helperFields } total } }",
188
+ "variables": {
189
+ "accountID": account_id,
190
+ "type": entity_type
191
+ },
192
+ "operationName": "entityLookup"
193
+ }
194
+ response = makeCall(args, configuration, entity_query)
195
+
196
+ # Check for GraphQL errors in snapshot response
197
+ if 'errors' in response:
198
+ error_messages = [error.get('message', 'Unknown error') for error in response['errors']]
199
+ raise Exception(f"Snapshot API returned errors: {', '.join(error_messages)}")
200
+
201
+ if not response or 'data' not in response or 'entityLookup' not in response['data']:
202
+ raise ValueError("Failed to retrieve snapshot data from API")
203
+
204
+ items = response['data']['entityLookup']['items']
205
+ if items is None:
206
+ items = []
207
+ if hasattr(args, 'verbose') and args.verbose:
208
+ print("No items found in entity lookup - "+ entity_type)
209
+ return items
210
+
211
+ def getAccountSnapshot(args, configuration, account_id, site_ids=None):
212
+ snapshot_query = {
213
+ "query": "query accountSnapshot ( $siteIDs:[ID!] $accountID:ID ) { accountSnapshot ( accountID:$accountID ) { id sites ( siteIDs:$siteIDs ) { id protoId connectivityStatusSiteSnapshot: connectivityStatus haStatusSiteSnapshot: haStatus { readiness wanConnectivity keepalive socketVersion } operationalStatusSiteSnapshot: operationalStatus lastConnected connectedSince popName devices { id name identifier connected haRole interfaces { connected id name physicalPort naturalOrder popName previousPopID previousPopName tunnelConnectionReason tunnelUptime tunnelRemoteIP tunnelRemoteIPInfoInterfaceSnapshot: tunnelRemoteIPInfo { ip countryCode countryName city state provider latitude longitude } type infoInterfaceSnapshot: info { id name upstreamBandwidth downstreamBandwidth upstreamBandwidthMbpsPrecision downstreamBandwidthMbpsPrecision destType wanRole } cellularInterfaceInfoInterfaceSnapshot: cellularInterfaceInfo { networkType simSlotId modemStatus isModemConnected iccid imei operatorName isModemSuspended apn apnSelectionMethod signalStrength isRoamingAllowed simNumber disconnectionReason isSimSlot1Detected isSimSlot2Detected } } lastConnected lastDuration connectedSince lastPopID lastPopName recentConnections { duration interfaceName deviceName lastConnected popName remoteIP remoteIPInfoRecentConnection: remoteIPInfo { ip countryCode countryName city state provider latitude longitude } } type deviceUptime socketInfo { id serial isPrimary platformSocketInfo: platform version versionUpdateTime } interfacesLinkState { id up mediaIn linkSpeed duplex hasAddress hasInternet hasTunnel } osType osVersion version versionNumber releaseGroup mfaExpirationTime mfaCreationTime internalIP } infoSiteSnapshot: info { name type description countryCode region countryName countryStateName cityName address isHA connType creationTime interfaces { id name upstreamBandwidth downstreamBandwidth upstreamBandwidthMbpsPrecision downstreamBandwidthMbpsPrecision destType wanRoleInterfaceInfo: wanRole } sockets { id serial isPrimary platformSocketInfo: platform version versionUpdateTime } ipsec { isPrimary catoIP remoteIP ikeVersion } } hostCount altWanStatus } users { id connectivityStatusUserSnapshot: connectivityStatus operationalStatusUserSnapshot: operationalStatus name deviceName uptime lastConnected version versionNumber popID popName remoteIP remoteIPInfoUserSnapshot: remoteIPInfo { ip countryCode countryName city state provider latitude longitude } internalIP osType osVersion devices { id name identifier connected haRole interfaces { connected id name physicalPort naturalOrder popName previousPopID previousPopName tunnelConnectionReason tunnelUptime tunnelRemoteIP tunnelRemoteIPInfoInterfaceSnapshot: tunnelRemoteIPInfo { ip countryCode countryName city state provider latitude longitude } type infoInterfaceSnapshot: info { id name upstreamBandwidth downstreamBandwidth upstreamBandwidthMbpsPrecision downstreamBandwidthMbpsPrecision destType wanRole } cellularInterfaceInfoInterfaceSnapshot: cellularInterfaceInfo { networkType simSlotId modemStatus isModemConnected iccid imei operatorName isModemSuspended apn apnSelectionMethod signalStrength isRoamingAllowed simNumber disconnectionReason isSimSlot1Detected isSimSlot2Detected } } lastConnected lastDuration connectedSince lastPopID lastPopName recentConnections { duration interfaceName deviceName lastConnected popName remoteIP remoteIPInfoRecentConnection: remoteIPInfo { ip countryCode countryName city state provider latitude longitude } } type deviceUptime socketInfo { id serial isPrimary platformSocketInfo: platform version versionUpdateTime } interfacesLinkState { id up mediaIn linkSpeed duplex hasAddress hasInternet hasTunnel } osType osVersion version versionNumber releaseGroup mfaExpirationTime mfaCreationTime internalIP } connectedInOffice infoUserSnapshot: info { name status email creationTime phoneNumber origin authMethod } recentConnections { duration interfaceName deviceName lastConnected popName remoteIP remoteIPInfo { ip countryCode countryName city state provider latitude longitude } } } timestamp } }",
214
+ "variables": {
215
+ "accountID": account_id,
216
+ "siteIDs": site_ids
217
+ },
218
+ "operationName": "accountSnapshot"
219
+ }
220
+ response = makeCall(args, configuration, snapshot_query)
221
+
222
+ # Check for GraphQL errors in snapshot response
223
+ if 'errors' in response:
224
+ error_messages = [error.get('message', 'Unknown error') for error in response['errors']]
225
+ raise Exception(f"Snapshot API returned errors: {', '.join(error_messages)}")
226
+
227
+ if not response or 'data' not in response or 'accountSnapshot' not in response['data']:
228
+ raise ValueError("Failed to retrieve snapshot data from API")
229
+
230
+ if not response or 'sites' not in response['data']['accountSnapshot'] or response['data']['accountSnapshot']['sites'] is None:
231
+ raise ValueError("No sites found in account snapshot data from API")
232
+
233
+ return response
@@ -11,7 +11,7 @@ def import_parse(subparsers):
11
11
  if_rules_parser = import_subparsers.add_parser(
12
12
  'if_rules_to_tf',
13
13
  help='Import Internet Firewall rules to Terraform state',
14
- usage='catocli import if_rules_to_tf <json_file> --module-name <module_name> [options]'
14
+ usage='catocli import if_rules_to_tf <json_file> --module-name <module_name> [options]\n\nexample: catocli import if_rules_to_tf config_data/all_wf_rules_and_sections.json --module-name module.if_rules'
15
15
  )
16
16
 
17
17
  if_rules_parser.add_argument('json_file', help='Path to the JSON file containing IFW rules and sections')
@@ -35,7 +35,7 @@ def import_parse(subparsers):
35
35
  wf_rules_parser = import_subparsers.add_parser(
36
36
  'wf_rules_to_tf',
37
37
  help='Import WAN Firewall rules to Terraform state',
38
- usage='catocli import wf_rules_to_tf <json_file> --module-name <module_name> [options]'
38
+ usage='catocli import wf_rules_to_tf <json_file> --module-name <module_name> [options]\n\nexample: catocli import wf_rules_to_tf config_data/all_wf_rules_and_sections.json --module-name module.wf_rules'
39
39
  )
40
40
 
41
41
  wf_rules_parser.add_argument('json_file', help='Path to the JSON file containing WF rules and sections')
@@ -60,14 +60,13 @@ def extract_rules_and_sections(policy_data):
60
60
  })
61
61
 
62
62
  # Extract sections
63
- section_ids = policy_data.get('section_ids', {})
64
63
  for section in policy_data.get('sections', []):
65
64
  if section.get('section_name'):
66
65
  sections.append({
67
66
  'section_name': section['section_name'],
68
67
  'section_index': section.get('section_index', 0),
69
- 'section_id': section_ids.get(section['section_name'], '')
70
- })
68
+ 'section_id': section.get('section_id', '')
69
+ })
71
70
  return rules, sections
72
71
 
73
72
 
@@ -123,8 +122,8 @@ def find_rule_index(rules, rule_name):
123
122
  return None
124
123
 
125
124
 
126
- def import_sections(sections, module_name, verbose=False,
127
- resource_type="cato_if_section", resource_name="sections"):
125
+ def import_sections(sections, module_name, resource_type,
126
+ resource_name="sections", verbose=False):
128
127
  """Import all sections"""
129
128
  print("\nStarting section imports...")
130
129
  total_sections = len(sections)
@@ -135,7 +134,7 @@ def import_sections(sections, module_name, verbose=False,
135
134
  section_id = section['section_id']
136
135
  section_name = section['section_name']
137
136
  section_index = section['section_index']
138
- resource_address = f'{module_name}.{resource_type}.{resource_name}["{str(section_name)}"]'
137
+ resource_address = f'{module_name}.{resource_type}.{resource_name}["{section_name}"]'
139
138
  print(f"\n[{i+1}/{total_sections}] Section: {section_name} (index: {section_index})")
140
139
 
141
140
  # For sections, we use the section name as the ID since that's how Cato identifies them
@@ -376,7 +375,7 @@ def import_if_rules_to_tf(args, configuration):
376
375
 
377
376
  # Import sections first (if not skipped)
378
377
  if not args.rules_only and sections:
379
- successful, failed = import_sections(sections, module_name=args.module_name, verbose=args.verbose)
378
+ successful, failed = import_sections(sections, module_name=args.module_name, resource_type="cato_if_section", verbose=args.verbose)
380
379
  total_successful += successful
381
380
  total_failed += failed
382
381
 
@@ -439,7 +438,10 @@ def import_wf_sections(sections, module_name, verbose=False,
439
438
  section_id = section['section_id']
440
439
  section_name = section['section_name']
441
440
  section_index = section['section_index']
442
- resource_address = f'{module_name}.{resource_type}.{resource_name}["{str(section_name)}"]'
441
+ # Add module. prefix if not present
442
+ if not module_name.startswith('module.'):
443
+ module_name = f'module.{module_name}'
444
+ resource_address = f'{module_name}.{resource_type}.{resource_name}["{section_name}"]'
443
445
  print(f"\n[{i+1}/{total_sections}] Section: {section_name} (index: {section_index})")
444
446
 
445
447
  # For sections, we use the section name as the ID since that's how Cato identifies them
@@ -469,6 +471,10 @@ def import_wf_rules(rules, module_name, verbose=False,
469
471
  rule_index = find_rule_index(rules, rule_name)
470
472
  terraform_key = sanitize_name_for_terraform(rule_name)
471
473
 
474
+ # Add module. prefix if not present
475
+ if not module_name.startswith('module.'):
476
+ module_name = f'module.{module_name}'
477
+
472
478
  # Use array index syntax instead of rule ID
473
479
  resource_address = f'{module_name}.{resource_type}.{resource_name}["{str(rule_name)}"]'
474
480
  print(f"\n[{i+1}/{total_rules}] Rule: {rule_name} (index: {rule_index})")
@@ -519,8 +525,12 @@ def import_wf_rules_to_tf(args, configuration):
519
525
  print(" No rules or sections found. Exiting.")
520
526
  return [{"success": False, "error": "No rules or sections found"}]
521
527
 
528
+ # Add module. prefix if not present
529
+ module_name = args.module_name
530
+ if not module_name.startswith('module.'):
531
+ module_name = f'module.{module_name}'
522
532
  # Validate Terraform environment before proceeding
523
- validate_terraform_environment(args.module_name, verbose=args.verbose)
533
+ validate_terraform_environment(module_name, verbose=args.verbose)
524
534
 
525
535
  # Ask for confirmation (unless auto-approved)
526
536
  if not args.rules_only and not args.sections_only: