catocli 1.0.21__py3-none-any.whl → 2.0.0__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 (139) hide show
  1. catocli/Utils/clidriver.py +112 -25
  2. catocli/Utils/profile_manager.py +188 -0
  3. catocli/Utils/version_checker.py +192 -0
  4. catocli/__init__.py +1 -1
  5. catocli/parsers/configure/__init__.py +115 -0
  6. catocli/parsers/configure/configure.py +307 -0
  7. catocli/parsers/custom/__init__.py +8 -0
  8. catocli/parsers/custom/export_rules/__init__.py +36 -0
  9. catocli/parsers/custom/export_rules/export_rules.py +361 -0
  10. catocli/parsers/custom/import_rules_to_tf/__init__.py +58 -0
  11. catocli/parsers/custom/import_rules_to_tf/import_rules_to_tf.py +577 -0
  12. catocli/parsers/mutation_admin_addAdmin/README.md +1 -1
  13. catocli/parsers/mutation_hardware/README.md +7 -0
  14. catocli/parsers/mutation_hardware/__init__.py +23 -0
  15. catocli/parsers/mutation_hardware_updateHardwareShipping/README.md +17 -0
  16. catocli/parsers/mutation_site_addBgpPeer/README.md +1 -1
  17. catocli/parsers/mutation_site_addNetworkRange/README.md +1 -1
  18. catocli/parsers/mutation_site_updateBgpPeer/README.md +1 -1
  19. catocli/parsers/mutation_site_updateNetworkRange/README.md +1 -1
  20. catocli/parsers/mutation_sites_addBgpPeer/README.md +1 -1
  21. catocli/parsers/mutation_sites_addNetworkRange/README.md +1 -1
  22. catocli/parsers/mutation_sites_updateBgpPeer/README.md +1 -1
  23. catocli/parsers/mutation_sites_updateNetworkRange/README.md +1 -1
  24. catocli/parsers/query_auditFeed/README.md +1 -1
  25. catocli/parsers/query_catalogs/README.md +19 -0
  26. catocli/parsers/query_catalogs/__init__.py +17 -0
  27. catocli/parsers/query_devices/README.md +19 -0
  28. catocli/parsers/query_devices/__init__.py +17 -0
  29. catocli/parsers/query_eventsFeed/README.md +1 -1
  30. catocli/parsers/query_hardware/README.md +17 -0
  31. catocli/parsers/query_hardware/__init__.py +17 -0
  32. catocli/parsers/query_sandbox/README.md +1 -1
  33. {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/METADATA +1 -1
  34. {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/RECORD +139 -114
  35. {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/top_level.txt +1 -0
  36. graphql_client/api/call_api.py +4 -0
  37. graphql_client/api_client_types.py +4 -3
  38. graphql_client/configuration.py +2 -0
  39. models/mutation.admin.addAdmin.json +130 -0
  40. models/mutation.hardware.updateHardwareShipping.json +2506 -0
  41. models/mutation.policy.appTenantRestriction.addRule.json +11 -11
  42. models/mutation.policy.appTenantRestriction.createPolicyRevision.json +11 -11
  43. models/mutation.policy.appTenantRestriction.discardPolicyRevision.json +11 -11
  44. models/mutation.policy.appTenantRestriction.moveRule.json +11 -11
  45. models/mutation.policy.appTenantRestriction.publishPolicyRevision.json +11 -11
  46. models/mutation.policy.appTenantRestriction.removeRule.json +11 -11
  47. models/mutation.policy.appTenantRestriction.updatePolicy.json +11 -11
  48. models/mutation.policy.appTenantRestriction.updateRule.json +11 -11
  49. models/mutation.policy.dynamicIpAllocation.addRule.json +4 -4
  50. models/mutation.policy.dynamicIpAllocation.createPolicyRevision.json +4 -4
  51. models/mutation.policy.dynamicIpAllocation.discardPolicyRevision.json +4 -4
  52. models/mutation.policy.dynamicIpAllocation.moveRule.json +4 -4
  53. models/mutation.policy.dynamicIpAllocation.publishPolicyRevision.json +4 -4
  54. models/mutation.policy.dynamicIpAllocation.removeRule.json +4 -4
  55. models/mutation.policy.dynamicIpAllocation.updatePolicy.json +4 -4
  56. models/mutation.policy.dynamicIpAllocation.updateRule.json +4 -4
  57. models/mutation.policy.internetFirewall.addRule.json +63 -63
  58. models/mutation.policy.internetFirewall.createPolicyRevision.json +45 -45
  59. models/mutation.policy.internetFirewall.discardPolicyRevision.json +45 -45
  60. models/mutation.policy.internetFirewall.moveRule.json +45 -45
  61. models/mutation.policy.internetFirewall.publishPolicyRevision.json +45 -45
  62. models/mutation.policy.internetFirewall.removeRule.json +45 -45
  63. models/mutation.policy.internetFirewall.updatePolicy.json +45 -45
  64. models/mutation.policy.internetFirewall.updateRule.json +63 -63
  65. models/mutation.policy.remotePortFwd.addRule.json +5 -5
  66. models/mutation.policy.remotePortFwd.createPolicyRevision.json +5 -5
  67. models/mutation.policy.remotePortFwd.discardPolicyRevision.json +5 -5
  68. models/mutation.policy.remotePortFwd.moveRule.json +5 -5
  69. models/mutation.policy.remotePortFwd.publishPolicyRevision.json +5 -5
  70. models/mutation.policy.remotePortFwd.removeRule.json +5 -5
  71. models/mutation.policy.remotePortFwd.updatePolicy.json +5 -5
  72. models/mutation.policy.remotePortFwd.updateRule.json +5 -5
  73. models/mutation.policy.socketLan.addRule.json +3580 -125
  74. models/mutation.policy.socketLan.createPolicyRevision.json +3580 -125
  75. models/mutation.policy.socketLan.discardPolicyRevision.json +3580 -125
  76. models/mutation.policy.socketLan.moveRule.json +3580 -125
  77. models/mutation.policy.socketLan.publishPolicyRevision.json +3580 -125
  78. models/mutation.policy.socketLan.removeRule.json +3580 -125
  79. models/mutation.policy.socketLan.updatePolicy.json +3580 -125
  80. models/mutation.policy.socketLan.updateRule.json +3580 -125
  81. models/mutation.policy.wanFirewall.addRule.json +77 -77
  82. models/mutation.policy.wanFirewall.createPolicyRevision.json +59 -59
  83. models/mutation.policy.wanFirewall.discardPolicyRevision.json +59 -59
  84. models/mutation.policy.wanFirewall.moveRule.json +59 -59
  85. models/mutation.policy.wanFirewall.publishPolicyRevision.json +59 -59
  86. models/mutation.policy.wanFirewall.removeRule.json +59 -59
  87. models/mutation.policy.wanFirewall.updatePolicy.json +59 -59
  88. models/mutation.policy.wanFirewall.updateRule.json +77 -77
  89. models/mutation.policy.wanNetwork.addRule.json +49 -49
  90. models/mutation.policy.wanNetwork.createPolicyRevision.json +49 -49
  91. models/mutation.policy.wanNetwork.discardPolicyRevision.json +49 -49
  92. models/mutation.policy.wanNetwork.moveRule.json +49 -49
  93. models/mutation.policy.wanNetwork.publishPolicyRevision.json +49 -49
  94. models/mutation.policy.wanNetwork.removeRule.json +49 -49
  95. models/mutation.policy.wanNetwork.updatePolicy.json +49 -49
  96. models/mutation.policy.wanNetwork.updateRule.json +49 -49
  97. models/mutation.site.addBgpPeer.json +2812 -217
  98. models/mutation.site.addNetworkRange.json +114 -0
  99. models/mutation.site.addSocketSite.json +18 -0
  100. models/mutation.site.removeBgpPeer.json +667 -1
  101. models/mutation.site.updateBgpPeer.json +3152 -559
  102. models/mutation.site.updateNetworkRange.json +114 -0
  103. models/mutation.sites.addBgpPeer.json +2812 -217
  104. models/mutation.sites.addNetworkRange.json +114 -0
  105. models/mutation.sites.addSocketSite.json +18 -0
  106. models/mutation.sites.removeBgpPeer.json +667 -1
  107. models/mutation.sites.updateBgpPeer.json +3152 -559
  108. models/mutation.sites.updateNetworkRange.json +114 -0
  109. models/mutation.xdr.addStoryComment.json +2 -2
  110. models/mutation.xdr.analystFeedback.json +182 -42
  111. models/mutation.xdr.deleteStoryComment.json +2 -2
  112. models/query.accountMetrics.json +112 -0
  113. models/query.accountSnapshot.json +62 -0
  114. models/query.admin.json +46 -0
  115. models/query.admins.json +46 -0
  116. models/query.appStats.json +528 -0
  117. models/query.appStatsTimeSeries.json +396 -0
  118. models/query.auditFeed.json +273 -3336
  119. models/query.catalogs.json +9840 -0
  120. models/query.devices.json +15469 -0
  121. models/query.events.json +4606 -4318
  122. models/query.eventsFeed.json +1167 -1095
  123. models/query.eventsTimeSeries.json +3459 -3243
  124. models/query.hardware.json +5730 -0
  125. models/query.hardwareManagement.json +8 -2
  126. models/query.licensing.json +3 -3
  127. models/query.policy.json +3743 -298
  128. models/query.sandbox.json +6 -4
  129. models/query.site.json +1329 -4
  130. models/query.xdr.stories.json +182 -42
  131. models/query.xdr.story.json +182 -42
  132. schema/catolib.py +105 -28
  133. scripts/catolib.py +62 -0
  134. scripts/export_if_rules_to_json.py +188 -0
  135. scripts/export_wf_rules_to_json.py +111 -0
  136. scripts/import_wf_rules_to_tfstate.py +331 -0
  137. {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/LICENSE +0 -0
  138. {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/WHEEL +0 -0
  139. {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -6,15 +6,15 @@ import catocli
6
6
  from graphql_client import Configuration
7
7
  from graphql_client.api_client import ApiException
8
8
  from ..parsers.parserApiClient import get_help
9
+ from .profile_manager import get_profile_manager
10
+ from .version_checker import check_for_updates, force_check_updates
9
11
  import traceback
10
12
  import sys
11
13
  sys.path.insert(0, 'vendor')
12
14
  import urllib3
13
15
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
14
- if "CATO_TOKEN" not in os.environ:
15
- print("Missing authentication, please set the CATO_TOKEN environment variable with your api key.")
16
- exit()
17
- CATO_TOKEN = os.getenv("CATO_TOKEN")
16
+ # Initialize profile manager
17
+ 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
@@ -22,6 +22,7 @@ from ..parsers.query_siteLocation import query_siteLocation_parse
22
22
  from ..parsers.mutation_accountManagement import mutation_accountManagement_parse
23
23
  from ..parsers.mutation_admin import mutation_admin_parse
24
24
  from ..parsers.mutation_container import mutation_container_parse
25
+ from ..parsers.mutation_hardware import mutation_hardware_parse
25
26
  from ..parsers.mutation_policy import mutation_policy_parse
26
27
  from ..parsers.mutation_sandbox import mutation_sandbox_parse
27
28
  from ..parsers.mutation_site import mutation_site_parse
@@ -37,11 +38,14 @@ from ..parsers.query_admins import query_admins_parse
37
38
  from ..parsers.query_appStats import query_appStats_parse
38
39
  from ..parsers.query_appStatsTimeSeries import query_appStatsTimeSeries_parse
39
40
  from ..parsers.query_auditFeed import query_auditFeed_parse
41
+ from ..parsers.query_catalogs import query_catalogs_parse
40
42
  from ..parsers.query_container import query_container_parse
43
+ from ..parsers.query_devices import query_devices_parse
41
44
  from ..parsers.query_entityLookup import query_entityLookup_parse
42
45
  from ..parsers.query_events import query_events_parse
43
46
  from ..parsers.query_eventsFeed import query_eventsFeed_parse
44
47
  from ..parsers.query_eventsTimeSeries import query_eventsTimeSeries_parse
48
+ from ..parsers.query_hardware import query_hardware_parse
45
49
  from ..parsers.query_hardwareManagement import query_hardwareManagement_parse
46
50
  from ..parsers.query_licensing import query_licensing_parse
47
51
  from ..parsers.query_policy import query_policy_parse
@@ -50,12 +54,56 @@ from ..parsers.query_site import query_site_parse
50
54
  from ..parsers.query_subDomains import query_subDomains_parse
51
55
  from ..parsers.query_xdr import query_xdr_parse
52
56
 
53
- configuration = Configuration()
54
- configuration.verify_ssl = False
55
- configuration.api_key["x-api-key"] = CATO_TOKEN
56
- configuration.host = "{}".format(catocli.__cato_host__)
57
- configuration.debug = CATO_DEBUG
58
- configuration.version = "{}".format(catocli.__version__)
57
+ def show_version_info(args, configuration=None):
58
+ print(f"catocli version {catocli.__version__}")
59
+
60
+ if not args.current_only:
61
+ if args.check_updates:
62
+ # Force check for updates
63
+ is_newer, latest_version, source = force_check_updates()
64
+ else:
65
+ # Regular check (uses cache)
66
+ is_newer, latest_version, source = check_for_updates(show_if_available=False)
67
+
68
+ if latest_version:
69
+ if is_newer:
70
+ print(f"Latest version: {latest_version} (from {source}) - UPDATE AVAILABLE!")
71
+ print()
72
+ print("To upgrade, run:")
73
+ print("pip install --upgrade catocli")
74
+ else:
75
+ print(f"Latest version: {latest_version} (from {source}) - You are up to date!")
76
+ else:
77
+ print("Unable to check for updates (check your internet connection)")
78
+ return [{"success": True, "current_version": catocli.__version__, "latest_version": latest_version if not args.current_only else None}]
79
+
80
+ def get_configuration():
81
+ configuration = Configuration()
82
+ configuration.verify_ssl = False
83
+ configuration.debug = CATO_DEBUG
84
+ configuration.version = "{}".format(catocli.__version__)
85
+
86
+ # Try to migrate from environment variables first
87
+ profile_manager.migrate_from_environment()
88
+
89
+ # Get credentials from profile
90
+ credentials = profile_manager.get_credentials()
91
+ if not credentials:
92
+ print("No Cato CLI profile configured.")
93
+ print("Run 'catocli configure set' to set up your credentials.")
94
+ exit(1)
95
+
96
+ if not credentials.get('cato_token') or not credentials.get('account_id'):
97
+ profile_name = profile_manager.get_current_profile()
98
+ print(f"Profile '{profile_name}' is missing required credentials.")
99
+ print(f"Run 'catocli configure set --profile {profile_name}' to update your credentials.")
100
+ exit(1)
101
+
102
+ configuration.api_key["x-api-key"] = credentials['cato_token']
103
+ configuration.host = credentials['endpoint']
104
+ configuration.accountID = credentials['account_id']
105
+
106
+ return configuration
59
107
 
60
108
  defaultReadmeStr = """
61
109
  The Cato CLI is a command-line interface tool designed to simplify the management and automation of Cato Networks’ configurations and operations.
@@ -70,7 +118,15 @@ https://github.com/catonetworks/cato-api-explorer
70
118
 
71
119
  parser = argparse.ArgumentParser(prog='catocli', usage='%(prog)s <operationType> <operationName> [options]', description=defaultReadmeStr)
72
120
  parser.add_argument('--version', action='version', version=catocli.__version__)
121
+ parser.add_argument('-H', '--header', action='append', dest='headers', help='Add custom headers in "Key: Value" format. Can be used multiple times.')
73
122
  subparsers = parser.add_subparsers()
123
+
124
+ # Version command - enhanced with update checking
125
+ version_parser = subparsers.add_parser('version', help='Show version information and check for updates')
126
+ version_parser.add_argument('--check-updates', action='store_true', help='Force check for updates (ignores cache)')
127
+ version_parser.add_argument('--current-only', action='store_true', help='Show only current version')
128
+ version_parser.set_defaults(func=show_version_info)
129
+
74
130
  custom_parsers = custom_parse(subparsers)
75
131
  raw_parsers = subparsers.add_parser('raw', help='Raw GraphQL', usage=get_help("raw"))
76
132
  raw_parser = raw_parse(raw_parsers)
@@ -83,6 +139,7 @@ mutation_subparsers = mutation_parser.add_subparsers(description='valid subcomma
83
139
  mutation_accountManagement_parser = mutation_accountManagement_parse(mutation_subparsers)
84
140
  mutation_admin_parser = mutation_admin_parse(mutation_subparsers)
85
141
  mutation_container_parser = mutation_container_parse(mutation_subparsers)
142
+ mutation_hardware_parser = mutation_hardware_parse(mutation_subparsers)
86
143
  mutation_policy_parser = mutation_policy_parse(mutation_subparsers)
87
144
  mutation_sandbox_parser = mutation_sandbox_parse(mutation_subparsers)
88
145
  mutation_site_parser = mutation_site_parse(mutation_subparsers)
@@ -98,11 +155,14 @@ query_admins_parser = query_admins_parse(query_subparsers)
98
155
  query_appStats_parser = query_appStats_parse(query_subparsers)
99
156
  query_appStatsTimeSeries_parser = query_appStatsTimeSeries_parse(query_subparsers)
100
157
  query_auditFeed_parser = query_auditFeed_parse(query_subparsers)
158
+ query_catalogs_parser = query_catalogs_parse(query_subparsers)
101
159
  query_container_parser = query_container_parse(query_subparsers)
160
+ query_devices_parser = query_devices_parse(query_subparsers)
102
161
  query_entityLookup_parser = query_entityLookup_parse(query_subparsers)
103
162
  query_events_parser = query_events_parse(query_subparsers)
104
163
  query_eventsFeed_parser = query_eventsFeed_parse(query_subparsers)
105
164
  query_eventsTimeSeries_parser = query_eventsTimeSeries_parse(query_subparsers)
165
+ query_hardware_parser = query_hardware_parse(query_subparsers)
106
166
  query_hardwareManagement_parser = query_hardwareManagement_parse(query_subparsers)
107
167
  query_licensing_parser = query_licensing_parse(query_subparsers)
108
168
  query_policy_parser = query_policy_parse(query_subparsers)
@@ -112,24 +172,51 @@ query_subDomains_parser = query_subDomains_parse(query_subparsers)
112
172
  query_xdr_parser = query_xdr_parse(query_subparsers)
113
173
 
114
174
 
175
+ def parse_headers(header_strings):
176
+ headers = {}
177
+ if header_strings:
178
+ for header_string in header_strings:
179
+ if ':' not in header_string:
180
+ print(f"ERROR: Invalid header format '{header_string}'. Use 'Key: Value' format.")
181
+ exit(1)
182
+ key, value = header_string.split(':', 1)
183
+ headers[key.strip()] = value.strip()
184
+ return headers
185
+
115
186
  def main(args=None):
187
+ # Check if no arguments provided or help is requested
188
+ if args is None:
189
+ args = sys.argv[1:]
190
+
191
+ # Show version check when displaying help or when no command specified
192
+ if not args or '-h' in args or '--help' in args:
193
+ # Check for updates in background (non-blocking)
194
+ try:
195
+ check_for_updates(show_if_available=True)
196
+ except Exception:
197
+ # Don't let version check interfere with CLI operation
198
+ pass
199
+
116
200
  args = parser.parse_args(args=args)
117
201
  try:
118
- CATO_ACCOUNT_ID = os.getenv("CATO_ACCOUNT_ID")
119
- if args.func.__name__!="createRawRequest":
120
- if CATO_ACCOUNT_ID==None and args.accountID==None:
121
- print("Missing accountID, please specify an accountID:\n")
122
- print('Option 1: Set the CATO_ACCOUNT_ID environment variable with the value of your account ID.')
123
- print('export CATO_ACCOUNT_ID="12345"\n')
124
- print("Option 2: Override the accountID value as a cli argument, example:")
125
- print('catocli <operationType> <operationName> -accountID=12345 <json>')
126
- print("catocli query entityLookup -accountID=12345 '{\"type\":\"country\"}'\n")
127
- exit()
128
- elif args.accountID!=None:
129
- configuration.accountID = args.accountID
130
- else:
131
- configuration.accountID = CATO_ACCOUNT_ID
132
- response = args.func(args, configuration)
202
+ # Skip authentication for configure commands
203
+ if hasattr(args, 'func') and hasattr(args.func, '__module__') and 'configure' in str(args.func.__module__):
204
+ response = args.func(args, None)
205
+ else:
206
+ # Get configuration from profiles
207
+ configuration = get_configuration()
208
+
209
+ # Parse custom headers if provided
210
+ if hasattr(args, 'headers') and args.headers:
211
+ custom_headers = parse_headers(args.headers)
212
+ configuration.custom_headers.update(custom_headers)
213
+ # Handle account ID override
214
+ if args.func.__name__ != "createRawRequest":
215
+ if hasattr(args, 'accountID') and args.accountID is not None:
216
+ # Command line override takes precedence
217
+ configuration.accountID = args.accountID
218
+ # Otherwise use the account ID from the profile (already set in get_configuration)
219
+ response = args.func(args, configuration)
133
220
 
134
221
  if type(response) == ApiException:
135
222
  print("ERROR! Status code: {}".format(response.status))
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Profile management for Cato CLI authentication
4
+ Supports AWS CLI-style profiles with credentials stored in ~/.cato/credentials
5
+ """
6
+
7
+ import os
8
+ import configparser
9
+ from pathlib import Path
10
+ import sys
11
+
12
+
13
+ class ProfileManager:
14
+ """Manages Cato CLI profiles and credentials"""
15
+
16
+ def __init__(self):
17
+ self.cato_dir = Path.home() / '.cato'
18
+ self.credentials_file = self.cato_dir / 'credentials'
19
+ self.config_file = self.cato_dir / 'config'
20
+ self.default_endpoint = "https://api.catonetworks.com/api/v1/graphql2"
21
+
22
+ def ensure_cato_directory(self):
23
+ """Ensure ~/.cato directory exists"""
24
+ self.cato_dir.mkdir(mode=0o700, exist_ok=True)
25
+
26
+ def get_profile_config(self, profile_name='default'):
27
+ """Get configuration for a specific profile"""
28
+ if not self.credentials_file.exists():
29
+ return None
30
+
31
+ config = configparser.ConfigParser()
32
+ config.read(self.credentials_file)
33
+
34
+ if profile_name not in config:
35
+ return None
36
+
37
+ profile_config = dict(config[profile_name])
38
+
39
+ # Ensure required fields have defaults
40
+ if 'endpoint' not in profile_config:
41
+ profile_config['endpoint'] = self.default_endpoint
42
+
43
+ return profile_config
44
+
45
+ def list_profiles(self):
46
+ """List all available profiles"""
47
+ if not self.credentials_file.exists():
48
+ return []
49
+
50
+ config = configparser.ConfigParser()
51
+ config.read(self.credentials_file)
52
+ return list(config.sections())
53
+
54
+ def create_profile(self, profile_name, endpoint=None, cato_token=None, account_id=None):
55
+ """Create or update a profile"""
56
+ self.ensure_cato_directory()
57
+
58
+ config = configparser.ConfigParser()
59
+ if self.credentials_file.exists():
60
+ config.read(self.credentials_file)
61
+
62
+ if profile_name not in config:
63
+ config.add_section(profile_name)
64
+
65
+ if endpoint:
66
+ config[profile_name]['endpoint'] = endpoint
67
+ elif 'endpoint' not in config[profile_name]:
68
+ config[profile_name]['endpoint'] = self.default_endpoint
69
+
70
+ if cato_token:
71
+ config[profile_name]['cato_token'] = cato_token
72
+
73
+ if account_id:
74
+ config[profile_name]['account_id'] = account_id
75
+
76
+ with open(self.credentials_file, 'w') as f:
77
+ config.write(f)
78
+
79
+ # Set secure permissions
80
+ os.chmod(self.credentials_file, 0o600)
81
+
82
+ return True
83
+
84
+ def delete_profile(self, profile_name):
85
+ """Delete a profile"""
86
+ if not self.credentials_file.exists():
87
+ return False
88
+
89
+ config = configparser.ConfigParser()
90
+ config.read(self.credentials_file)
91
+
92
+ if profile_name not in config:
93
+ return False
94
+
95
+ config.remove_section(profile_name)
96
+
97
+ with open(self.credentials_file, 'w') as f:
98
+ config.write(f)
99
+
100
+ return True
101
+
102
+ def get_current_profile(self):
103
+ """Get the current active profile name"""
104
+ # Check environment variable first
105
+ profile = os.getenv('CATO_PROFILE')
106
+ if profile:
107
+ return profile
108
+
109
+ # Check config file
110
+ if self.config_file.exists():
111
+ config = configparser.ConfigParser()
112
+ config.read(self.config_file)
113
+ if 'default' in config and 'profile' in config['default']:
114
+ return config['default']['profile']
115
+
116
+ return 'default'
117
+
118
+ def set_current_profile(self, profile_name):
119
+ """Set the current active profile"""
120
+ self.ensure_cato_directory()
121
+
122
+ config = configparser.ConfigParser()
123
+ if self.config_file.exists():
124
+ config.read(self.config_file)
125
+
126
+ if 'default' not in config:
127
+ config.add_section('default')
128
+
129
+ config['default']['profile'] = profile_name
130
+
131
+ with open(self.config_file, 'w') as f:
132
+ config.write(f)
133
+
134
+ return True
135
+
136
+ def get_credentials(self, profile_name=None):
137
+ """Get credentials for the specified or current profile"""
138
+ if profile_name is None:
139
+ profile_name = self.get_current_profile()
140
+
141
+ profile_config = self.get_profile_config(profile_name)
142
+ if not profile_config:
143
+ return None
144
+
145
+ return {
146
+ 'endpoint': profile_config.get('endpoint', self.default_endpoint),
147
+ 'cato_token': profile_config.get('cato_token'),
148
+ 'account_id': profile_config.get('account_id')
149
+ }
150
+
151
+ def validate_profile(self, profile_name=None):
152
+ """Validate that a profile has all required credentials"""
153
+ credentials = self.get_credentials(profile_name)
154
+ if not credentials:
155
+ return False, f"Profile '{profile_name or self.get_current_profile()}' not found"
156
+
157
+ missing = []
158
+ if not credentials.get('cato_token'):
159
+ missing.append('cato_token')
160
+ if not credentials.get('account_id'):
161
+ missing.append('account_id')
162
+
163
+ if missing:
164
+ return False, f"Profile missing required fields: {', '.join(missing)}"
165
+
166
+ return True, "Profile is valid"
167
+
168
+ def migrate_from_environment(self):
169
+ """Migrate from environment variables to default profile if needed"""
170
+ cato_token = os.getenv('CATO_TOKEN')
171
+ account_id = os.getenv('CATO_ACCOUNT_ID')
172
+
173
+ if cato_token and account_id:
174
+ # Check if default profile already exists
175
+ default_config = self.get_profile_config('default')
176
+ if not default_config:
177
+ print("Migrating environment variables to default profile...")
178
+ self.create_profile('default',
179
+ endpoint=self.default_endpoint,
180
+ cato_token=cato_token,
181
+ account_id=account_id)
182
+ return True
183
+ return False
184
+
185
+
186
+ def get_profile_manager():
187
+ """Get a ProfileManager instance"""
188
+ return ProfileManager()
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Version checking utility for Cato CLI
4
+ Checks for newer versions available on GitHub releases and PyPI
5
+ """
6
+
7
+ import json
8
+ import urllib.request
9
+ import urllib.error
10
+ import ssl
11
+ import os
12
+ import time
13
+ from .. import __version__
14
+
15
+ # Cache settings
16
+ CACHE_FILE = os.path.expanduser("~/.catocli_version_cache")
17
+ CACHE_DURATION = 3600 * 24 # 24 hours in seconds
18
+
19
+ def get_cached_version_info():
20
+ """Get cached version information if still valid"""
21
+ try:
22
+ if os.path.exists(CACHE_FILE):
23
+ with open(CACHE_FILE, 'r') as f:
24
+ cache_data = json.load(f)
25
+
26
+ # Check if cache is still valid
27
+ if time.time() - cache_data.get('timestamp', 0) < CACHE_DURATION:
28
+ return cache_data.get('latest_version'), cache_data.get('source')
29
+ except (json.JSONDecodeError, OSError):
30
+ pass
31
+ return None, None
32
+
33
+ def cache_version_info(latest_version, source):
34
+ """Cache version information"""
35
+ try:
36
+ cache_data = {
37
+ 'latest_version': latest_version,
38
+ 'source': source,
39
+ 'timestamp': time.time()
40
+ }
41
+ with open(CACHE_FILE, 'w') as f:
42
+ json.dump(cache_data, f)
43
+ except OSError:
44
+ pass # Fail silently if we can't write cache
45
+
46
+ def get_latest_github_version():
47
+ """Get the latest version from GitHub releases"""
48
+ try:
49
+ # Create SSL context that doesn't verify certificates (for corporate networks)
50
+ context = ssl.create_default_context()
51
+ context.check_hostname = False
52
+ context.verify_mode = ssl.CERT_NONE
53
+
54
+ url = "https://api.github.com/repos/catonetworks/cato-cli/releases/latest"
55
+ req = urllib.request.Request(url)
56
+ req.add_header('User-Agent', f'catocli/{__version__}')
57
+
58
+ with urllib.request.urlopen(req, context=context, timeout=5) as response:
59
+ data = json.loads(response.read().decode())
60
+ tag_name = data.get('tag_name', '')
61
+ # Remove 'v' prefix if present
62
+ if tag_name.startswith('v'):
63
+ tag_name = tag_name[1:]
64
+ return tag_name
65
+ except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, KeyError):
66
+ return None
67
+
68
+ def get_latest_pypi_version():
69
+ """Get the latest version from PyPI"""
70
+ try:
71
+ # Create SSL context that doesn't verify certificates (for corporate networks)
72
+ context = ssl.create_default_context()
73
+ context.check_hostname = False
74
+ context.verify_mode = ssl.CERT_NONE
75
+
76
+ url = "https://pypi.org/pypi/catocli/json"
77
+ req = urllib.request.Request(url)
78
+ req.add_header('User-Agent', f'catocli/{__version__}')
79
+
80
+ with urllib.request.urlopen(req, context=context, timeout=5) as response:
81
+ data = json.loads(response.read().decode())
82
+ return data['info']['version']
83
+ except (urllib.error.URLError, urllib.error.HTTPError, json.JSONDecodeError, KeyError):
84
+ return None
85
+
86
+ def get_latest_version():
87
+ """Get the latest version available from GitHub or PyPI"""
88
+ # Check cache first
89
+ cached_version, cached_source = get_cached_version_info()
90
+ if cached_version:
91
+ return cached_version, cached_source
92
+
93
+ # Try GitHub first (usually more up-to-date for development releases)
94
+ github_version = get_latest_github_version()
95
+ if github_version:
96
+ cache_version_info(github_version, 'GitHub')
97
+ return github_version, 'GitHub'
98
+
99
+ # Fall back to PyPI
100
+ pypi_version = get_latest_pypi_version()
101
+ if pypi_version:
102
+ cache_version_info(pypi_version, 'PyPI')
103
+ return pypi_version, 'PyPI'
104
+
105
+ return None, None
106
+
107
+ def compare_versions(version1, version2):
108
+ """Compare two version strings. Returns 1 if version1 > version2, -1 if version1 < version2, 0 if equal"""
109
+ def version_tuple(v):
110
+ # Convert version string to tuple of integers for comparison
111
+ # Handle versions like "1.0.20", "1.0.20-beta", etc.
112
+ parts = v.split('-')[0].split('.') # Remove pre-release suffixes
113
+ return tuple(int(x) for x in parts if x.isdigit())
114
+
115
+ try:
116
+ v1_tuple = version_tuple(version1)
117
+ v2_tuple = version_tuple(version2)
118
+
119
+ # Pad shorter version with zeros
120
+ max_len = max(len(v1_tuple), len(v2_tuple))
121
+ v1_tuple += (0,) * (max_len - len(v1_tuple))
122
+ v2_tuple += (0,) * (max_len - len(v2_tuple))
123
+
124
+ if v1_tuple > v2_tuple:
125
+ return 1
126
+ elif v1_tuple < v2_tuple:
127
+ return -1
128
+ else:
129
+ return 0
130
+ except (ValueError, AttributeError):
131
+ return 0 # If we can't parse, assume they're equal
132
+
133
+ def is_newer_version_available():
134
+ """Check if a newer version is available"""
135
+ try:
136
+ latest_version, source = get_latest_version()
137
+ if latest_version:
138
+ comparison = compare_versions(latest_version, __version__)
139
+ return comparison > 0, latest_version, source
140
+ except Exception:
141
+ pass # Fail silently for any version parsing errors
142
+
143
+ return False, None, None
144
+
145
+ def show_upgrade_message(latest_version, source):
146
+ """Display upgrade message to user"""
147
+ print()
148
+ print("─" * 60)
149
+ print(f"🚀 A newer version of catocli is available!")
150
+ print(f" Current version: {__version__}")
151
+ print(f" Latest version: {latest_version} (from {source})")
152
+ print()
153
+ if source == 'PyPI':
154
+ print(" To upgrade, run:")
155
+ print(" pip install --upgrade catocli")
156
+ else:
157
+ print(" To upgrade, run:")
158
+ print(" pip install --upgrade catocli")
159
+ print(" (or check GitHub releases for pre-release versions)")
160
+ print("─" * 60)
161
+ print()
162
+
163
+ def check_for_updates(show_if_available=True):
164
+ """
165
+ Check for updates and optionally show upgrade message
166
+
167
+ Args:
168
+ show_if_available (bool): Whether to show the upgrade message if update is available
169
+
170
+ Returns:
171
+ tuple: (is_newer_available, latest_version, source)
172
+ """
173
+ try:
174
+ is_newer, latest_version, source = is_newer_version_available()
175
+
176
+ if is_newer and show_if_available:
177
+ show_upgrade_message(latest_version, source)
178
+
179
+ return is_newer, latest_version, source
180
+ except Exception:
181
+ # Fail silently - don't interrupt the user's workflow
182
+ return False, None, None
183
+
184
+ def force_check_updates():
185
+ """Force check for updates by clearing cache"""
186
+ try:
187
+ if os.path.exists(CACHE_FILE):
188
+ os.remove(CACHE_FILE)
189
+ except OSError:
190
+ pass
191
+
192
+ return check_for_updates(show_if_available=True)
catocli/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "1.0.21"
1
+ __version__ = "2.0.0"
2
2
  __cato_host__ = "https://api.catonetworks.com/api/v1/graphql2"