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.
- catocli/Utils/clidriver.py +112 -25
- catocli/Utils/profile_manager.py +188 -0
- catocli/Utils/version_checker.py +192 -0
- catocli/__init__.py +1 -1
- catocli/parsers/configure/__init__.py +115 -0
- catocli/parsers/configure/configure.py +307 -0
- catocli/parsers/custom/__init__.py +8 -0
- catocli/parsers/custom/export_rules/__init__.py +36 -0
- catocli/parsers/custom/export_rules/export_rules.py +361 -0
- catocli/parsers/custom/import_rules_to_tf/__init__.py +58 -0
- catocli/parsers/custom/import_rules_to_tf/import_rules_to_tf.py +577 -0
- catocli/parsers/mutation_admin_addAdmin/README.md +1 -1
- catocli/parsers/mutation_hardware/README.md +7 -0
- catocli/parsers/mutation_hardware/__init__.py +23 -0
- catocli/parsers/mutation_hardware_updateHardwareShipping/README.md +17 -0
- catocli/parsers/mutation_site_addBgpPeer/README.md +1 -1
- catocli/parsers/mutation_site_addNetworkRange/README.md +1 -1
- catocli/parsers/mutation_site_updateBgpPeer/README.md +1 -1
- catocli/parsers/mutation_site_updateNetworkRange/README.md +1 -1
- catocli/parsers/mutation_sites_addBgpPeer/README.md +1 -1
- catocli/parsers/mutation_sites_addNetworkRange/README.md +1 -1
- catocli/parsers/mutation_sites_updateBgpPeer/README.md +1 -1
- catocli/parsers/mutation_sites_updateNetworkRange/README.md +1 -1
- catocli/parsers/query_auditFeed/README.md +1 -1
- catocli/parsers/query_catalogs/README.md +19 -0
- catocli/parsers/query_catalogs/__init__.py +17 -0
- catocli/parsers/query_devices/README.md +19 -0
- catocli/parsers/query_devices/__init__.py +17 -0
- catocli/parsers/query_eventsFeed/README.md +1 -1
- catocli/parsers/query_hardware/README.md +17 -0
- catocli/parsers/query_hardware/__init__.py +17 -0
- catocli/parsers/query_sandbox/README.md +1 -1
- {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/METADATA +1 -1
- {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/RECORD +139 -114
- {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/top_level.txt +1 -0
- graphql_client/api/call_api.py +4 -0
- graphql_client/api_client_types.py +4 -3
- graphql_client/configuration.py +2 -0
- models/mutation.admin.addAdmin.json +130 -0
- models/mutation.hardware.updateHardwareShipping.json +2506 -0
- models/mutation.policy.appTenantRestriction.addRule.json +11 -11
- models/mutation.policy.appTenantRestriction.createPolicyRevision.json +11 -11
- models/mutation.policy.appTenantRestriction.discardPolicyRevision.json +11 -11
- models/mutation.policy.appTenantRestriction.moveRule.json +11 -11
- models/mutation.policy.appTenantRestriction.publishPolicyRevision.json +11 -11
- models/mutation.policy.appTenantRestriction.removeRule.json +11 -11
- models/mutation.policy.appTenantRestriction.updatePolicy.json +11 -11
- models/mutation.policy.appTenantRestriction.updateRule.json +11 -11
- models/mutation.policy.dynamicIpAllocation.addRule.json +4 -4
- models/mutation.policy.dynamicIpAllocation.createPolicyRevision.json +4 -4
- models/mutation.policy.dynamicIpAllocation.discardPolicyRevision.json +4 -4
- models/mutation.policy.dynamicIpAllocation.moveRule.json +4 -4
- models/mutation.policy.dynamicIpAllocation.publishPolicyRevision.json +4 -4
- models/mutation.policy.dynamicIpAllocation.removeRule.json +4 -4
- models/mutation.policy.dynamicIpAllocation.updatePolicy.json +4 -4
- models/mutation.policy.dynamicIpAllocation.updateRule.json +4 -4
- models/mutation.policy.internetFirewall.addRule.json +63 -63
- models/mutation.policy.internetFirewall.createPolicyRevision.json +45 -45
- models/mutation.policy.internetFirewall.discardPolicyRevision.json +45 -45
- models/mutation.policy.internetFirewall.moveRule.json +45 -45
- models/mutation.policy.internetFirewall.publishPolicyRevision.json +45 -45
- models/mutation.policy.internetFirewall.removeRule.json +45 -45
- models/mutation.policy.internetFirewall.updatePolicy.json +45 -45
- models/mutation.policy.internetFirewall.updateRule.json +63 -63
- models/mutation.policy.remotePortFwd.addRule.json +5 -5
- models/mutation.policy.remotePortFwd.createPolicyRevision.json +5 -5
- models/mutation.policy.remotePortFwd.discardPolicyRevision.json +5 -5
- models/mutation.policy.remotePortFwd.moveRule.json +5 -5
- models/mutation.policy.remotePortFwd.publishPolicyRevision.json +5 -5
- models/mutation.policy.remotePortFwd.removeRule.json +5 -5
- models/mutation.policy.remotePortFwd.updatePolicy.json +5 -5
- models/mutation.policy.remotePortFwd.updateRule.json +5 -5
- models/mutation.policy.socketLan.addRule.json +3580 -125
- models/mutation.policy.socketLan.createPolicyRevision.json +3580 -125
- models/mutation.policy.socketLan.discardPolicyRevision.json +3580 -125
- models/mutation.policy.socketLan.moveRule.json +3580 -125
- models/mutation.policy.socketLan.publishPolicyRevision.json +3580 -125
- models/mutation.policy.socketLan.removeRule.json +3580 -125
- models/mutation.policy.socketLan.updatePolicy.json +3580 -125
- models/mutation.policy.socketLan.updateRule.json +3580 -125
- models/mutation.policy.wanFirewall.addRule.json +77 -77
- models/mutation.policy.wanFirewall.createPolicyRevision.json +59 -59
- models/mutation.policy.wanFirewall.discardPolicyRevision.json +59 -59
- models/mutation.policy.wanFirewall.moveRule.json +59 -59
- models/mutation.policy.wanFirewall.publishPolicyRevision.json +59 -59
- models/mutation.policy.wanFirewall.removeRule.json +59 -59
- models/mutation.policy.wanFirewall.updatePolicy.json +59 -59
- models/mutation.policy.wanFirewall.updateRule.json +77 -77
- models/mutation.policy.wanNetwork.addRule.json +49 -49
- models/mutation.policy.wanNetwork.createPolicyRevision.json +49 -49
- models/mutation.policy.wanNetwork.discardPolicyRevision.json +49 -49
- models/mutation.policy.wanNetwork.moveRule.json +49 -49
- models/mutation.policy.wanNetwork.publishPolicyRevision.json +49 -49
- models/mutation.policy.wanNetwork.removeRule.json +49 -49
- models/mutation.policy.wanNetwork.updatePolicy.json +49 -49
- models/mutation.policy.wanNetwork.updateRule.json +49 -49
- models/mutation.site.addBgpPeer.json +2812 -217
- models/mutation.site.addNetworkRange.json +114 -0
- models/mutation.site.addSocketSite.json +18 -0
- models/mutation.site.removeBgpPeer.json +667 -1
- models/mutation.site.updateBgpPeer.json +3152 -559
- models/mutation.site.updateNetworkRange.json +114 -0
- models/mutation.sites.addBgpPeer.json +2812 -217
- models/mutation.sites.addNetworkRange.json +114 -0
- models/mutation.sites.addSocketSite.json +18 -0
- models/mutation.sites.removeBgpPeer.json +667 -1
- models/mutation.sites.updateBgpPeer.json +3152 -559
- models/mutation.sites.updateNetworkRange.json +114 -0
- models/mutation.xdr.addStoryComment.json +2 -2
- models/mutation.xdr.analystFeedback.json +182 -42
- models/mutation.xdr.deleteStoryComment.json +2 -2
- models/query.accountMetrics.json +112 -0
- models/query.accountSnapshot.json +62 -0
- models/query.admin.json +46 -0
- models/query.admins.json +46 -0
- models/query.appStats.json +528 -0
- models/query.appStatsTimeSeries.json +396 -0
- models/query.auditFeed.json +273 -3336
- models/query.catalogs.json +9840 -0
- models/query.devices.json +15469 -0
- models/query.events.json +4606 -4318
- models/query.eventsFeed.json +1167 -1095
- models/query.eventsTimeSeries.json +3459 -3243
- models/query.hardware.json +5730 -0
- models/query.hardwareManagement.json +8 -2
- models/query.licensing.json +3 -3
- models/query.policy.json +3743 -298
- models/query.sandbox.json +6 -4
- models/query.site.json +1329 -4
- models/query.xdr.stories.json +182 -42
- models/query.xdr.story.json +182 -42
- schema/catolib.py +105 -28
- scripts/catolib.py +62 -0
- scripts/export_if_rules_to_json.py +188 -0
- scripts/export_wf_rules_to_json.py +111 -0
- scripts/import_wf_rules_to_tfstate.py +331 -0
- {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/LICENSE +0 -0
- {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/WHEEL +0 -0
- {catocli-1.0.21.dist-info → catocli-2.0.0.dist-info}/entry_points.txt +0 -0
schema/catolib.py
CHANGED
|
@@ -503,8 +503,6 @@ def writeCliDriver(catoApiSchema):
|
|
|
503
503
|
parsersIndex[operationNameAry[0]+"_"+operationNameAry[1]] = True
|
|
504
504
|
parsers = list(parsersIndex.keys())
|
|
505
505
|
|
|
506
|
-
|
|
507
|
-
|
|
508
506
|
cliDriverStr = """
|
|
509
507
|
import os
|
|
510
508
|
import argparse
|
|
@@ -513,15 +511,15 @@ import catocli
|
|
|
513
511
|
from graphql_client import Configuration
|
|
514
512
|
from graphql_client.api_client import ApiException
|
|
515
513
|
from ..parsers.parserApiClient import get_help
|
|
514
|
+
from .profile_manager import get_profile_manager
|
|
515
|
+
from .version_checker import check_for_updates, force_check_updates
|
|
516
516
|
import traceback
|
|
517
517
|
import sys
|
|
518
518
|
sys.path.insert(0, 'vendor')
|
|
519
519
|
import urllib3
|
|
520
520
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
exit()
|
|
524
|
-
CATO_TOKEN = os.getenv("CATO_TOKEN")
|
|
521
|
+
# Initialize profile manager
|
|
522
|
+
profile_manager = get_profile_manager()
|
|
525
523
|
CATO_DEBUG = bool(os.getenv("CATO_DEBUG", False))
|
|
526
524
|
from ..parsers.raw import raw_parse
|
|
527
525
|
from ..parsers.custom import custom_parse
|
|
@@ -531,12 +529,56 @@ from ..parsers.query_siteLocation import query_siteLocation_parse
|
|
|
531
529
|
cliDriverStr += "from ..parsers."+parserName+" import "+parserName+"_parse\n"
|
|
532
530
|
|
|
533
531
|
cliDriverStr += """
|
|
534
|
-
configuration
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
532
|
+
def show_version_info(args, configuration=None):
|
|
533
|
+
print(f"catocli version {catocli.__version__}")
|
|
534
|
+
|
|
535
|
+
if not args.current_only:
|
|
536
|
+
if args.check_updates:
|
|
537
|
+
# Force check for updates
|
|
538
|
+
is_newer, latest_version, source = force_check_updates()
|
|
539
|
+
else:
|
|
540
|
+
# Regular check (uses cache)
|
|
541
|
+
is_newer, latest_version, source = check_for_updates(show_if_available=False)
|
|
542
|
+
|
|
543
|
+
if latest_version:
|
|
544
|
+
if is_newer:
|
|
545
|
+
print(f"Latest version: {latest_version} (from {source}) - UPDATE AVAILABLE!")
|
|
546
|
+
print()
|
|
547
|
+
print("To upgrade, run:")
|
|
548
|
+
print("pip install --upgrade catocli")
|
|
549
|
+
else:
|
|
550
|
+
print(f"Latest version: {latest_version} (from {source}) - You are up to date!")
|
|
551
|
+
else:
|
|
552
|
+
print("Unable to check for updates (check your internet connection)")
|
|
553
|
+
return [{"success": True, "current_version": catocli.__version__, "latest_version": latest_version if not args.current_only else None}]
|
|
554
|
+
|
|
555
|
+
def get_configuration():
|
|
556
|
+
configuration = Configuration()
|
|
557
|
+
configuration.verify_ssl = False
|
|
558
|
+
configuration.debug = CATO_DEBUG
|
|
559
|
+
configuration.version = "{}".format(catocli.__version__)
|
|
560
|
+
|
|
561
|
+
# Try to migrate from environment variables first
|
|
562
|
+
profile_manager.migrate_from_environment()
|
|
563
|
+
|
|
564
|
+
# Get credentials from profile
|
|
565
|
+
credentials = profile_manager.get_credentials()
|
|
566
|
+
if not credentials:
|
|
567
|
+
print("No Cato CLI profile configured.")
|
|
568
|
+
print("Run 'catocli configure set' to set up your credentials.")
|
|
569
|
+
exit(1)
|
|
570
|
+
|
|
571
|
+
if not credentials.get('cato_token') or not credentials.get('account_id'):
|
|
572
|
+
profile_name = profile_manager.get_current_profile()
|
|
573
|
+
print(f"Profile '{profile_name}' is missing required credentials.")
|
|
574
|
+
print(f"Run 'catocli configure set --profile {profile_name}' to update your credentials.")
|
|
575
|
+
exit(1)
|
|
576
|
+
|
|
577
|
+
configuration.api_key["x-api-key"] = credentials['cato_token']
|
|
578
|
+
configuration.host = credentials['endpoint']
|
|
579
|
+
configuration.accountID = credentials['account_id']
|
|
580
|
+
|
|
581
|
+
return configuration
|
|
540
582
|
|
|
541
583
|
defaultReadmeStr = \"""
|
|
542
584
|
The Cato CLI is a command-line interface tool designed to simplify the management and automation of Cato Networks’ configurations and operations.
|
|
@@ -547,7 +589,15 @@ https://github.com/catonetworks/cato-api-explorer
|
|
|
547
589
|
|
|
548
590
|
parser = argparse.ArgumentParser(prog='catocli', usage='%(prog)s <operationType> <operationName> [options]', description=defaultReadmeStr)
|
|
549
591
|
parser.add_argument('--version', action='version', version=catocli.__version__)
|
|
592
|
+
parser.add_argument('-H', '--header', action='append', dest='headers', help='Add custom headers in "Key: Value" format. Can be used multiple times.')
|
|
550
593
|
subparsers = parser.add_subparsers()
|
|
594
|
+
|
|
595
|
+
# Version command - enhanced with update checking
|
|
596
|
+
version_parser = subparsers.add_parser('version', help='Show version information and check for updates')
|
|
597
|
+
version_parser.add_argument('--check-updates', action='store_true', help='Force check for updates (ignores cache)')
|
|
598
|
+
version_parser.add_argument('--current-only', action='store_true', help='Show only current version')
|
|
599
|
+
version_parser.set_defaults(func=show_version_info)
|
|
600
|
+
|
|
551
601
|
custom_parsers = custom_parse(subparsers)
|
|
552
602
|
raw_parsers = subparsers.add_parser('raw', help='Raw GraphQL', usage=get_help("raw"))
|
|
553
603
|
raw_parser = raw_parse(raw_parsers)
|
|
@@ -563,24 +613,51 @@ mutation_subparsers = mutation_parser.add_subparsers(description='valid subcomma
|
|
|
563
613
|
|
|
564
614
|
cliDriverStr += """
|
|
565
615
|
|
|
616
|
+
def parse_headers(header_strings):
|
|
617
|
+
headers = {}
|
|
618
|
+
if header_strings:
|
|
619
|
+
for header_string in header_strings:
|
|
620
|
+
if ':' not in header_string:
|
|
621
|
+
print(f"ERROR: Invalid header format '{header_string}'. Use 'Key: Value' format.")
|
|
622
|
+
exit(1)
|
|
623
|
+
key, value = header_string.split(':', 1)
|
|
624
|
+
headers[key.strip()] = value.strip()
|
|
625
|
+
return headers
|
|
626
|
+
|
|
566
627
|
def main(args=None):
|
|
628
|
+
# Check if no arguments provided or help is requested
|
|
629
|
+
if args is None:
|
|
630
|
+
args = sys.argv[1:]
|
|
631
|
+
|
|
632
|
+
# Show version check when displaying help or when no command specified
|
|
633
|
+
if not args or '-h' in args or '--help' in args:
|
|
634
|
+
# Check for updates in background (non-blocking)
|
|
635
|
+
try:
|
|
636
|
+
check_for_updates(show_if_available=True)
|
|
637
|
+
except Exception:
|
|
638
|
+
# Don't let version check interfere with CLI operation
|
|
639
|
+
pass
|
|
640
|
+
|
|
567
641
|
args = parser.parse_args(args=args)
|
|
568
642
|
try:
|
|
569
|
-
|
|
570
|
-
if args.func.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
643
|
+
# Skip authentication for configure commands
|
|
644
|
+
if hasattr(args, 'func') and hasattr(args.func, '__module__') and 'configure' in str(args.func.__module__):
|
|
645
|
+
response = args.func(args, None)
|
|
646
|
+
else:
|
|
647
|
+
# Get configuration from profiles
|
|
648
|
+
configuration = get_configuration()
|
|
649
|
+
|
|
650
|
+
# Parse custom headers if provided
|
|
651
|
+
if hasattr(args, 'headers') and args.headers:
|
|
652
|
+
custom_headers = parse_headers(args.headers)
|
|
653
|
+
configuration.custom_headers.update(custom_headers)
|
|
654
|
+
# Handle account ID override
|
|
655
|
+
if args.func.__name__ != "createRawRequest":
|
|
656
|
+
if hasattr(args, 'accountID') and args.accountID is not None:
|
|
657
|
+
# Command line override takes precedence
|
|
658
|
+
configuration.accountID = args.accountID
|
|
659
|
+
# Otherwise use the account ID from the profile (already set in get_configuration)
|
|
660
|
+
response = args.func(args, configuration)
|
|
584
661
|
|
|
585
662
|
if type(response) == ApiException:
|
|
586
663
|
print("ERROR! Status code: {}".format(response.status))
|
|
@@ -932,7 +1009,7 @@ def generateGraphqlPayload(variablesObj,operation,operationName):
|
|
|
932
1009
|
queryStr += ") {\n" + renderArgsAndFields("", variablesObj, operation, operation["type"]["definition"], " ") + " }"
|
|
933
1010
|
queryStr += indent + "\n}";
|
|
934
1011
|
body = {
|
|
935
|
-
"query":queryStr.replace("\n", " ").replace("\t", " ").replace("
|
|
1012
|
+
"query":queryStr.replace("\n", " ").replace("\t", " ").replace(" ", " ").replace(" ", " ").replace(" ", " "),
|
|
936
1013
|
"variables":variablesObj,
|
|
937
1014
|
"operationName":renderCamelCase(".".join(operationAry)),
|
|
938
1015
|
}
|
scripts/catolib.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Any, Dict, List, Union
|
|
7
|
+
|
|
8
|
+
def should_strip_id(obj: Dict[str, Any]) -> bool:
|
|
9
|
+
if not isinstance(obj, dict):
|
|
10
|
+
return False
|
|
11
|
+
|
|
12
|
+
keys = set(obj.keys())
|
|
13
|
+
return keys == {'name', 'id'}
|
|
14
|
+
|
|
15
|
+
def strip_ids_recursive(obj: Union[Dict, List, Any]) -> Union[Dict, List, Any, None]:
|
|
16
|
+
if isinstance(obj, dict):
|
|
17
|
+
new_obj = {}
|
|
18
|
+
for key, value in obj.items():
|
|
19
|
+
## Ignoring sections to retain id and name pair
|
|
20
|
+
if key == 'sections':
|
|
21
|
+
new_obj[key] = value
|
|
22
|
+
else:
|
|
23
|
+
processed_value = strip_ids_recursive(value)
|
|
24
|
+
# Skip keys where the value is an empty list
|
|
25
|
+
if processed_value != []:
|
|
26
|
+
new_obj[key] = processed_value
|
|
27
|
+
# Check if this object should have its id stripped
|
|
28
|
+
if should_strip_id(new_obj):
|
|
29
|
+
return {'name': new_obj['name']}
|
|
30
|
+
return new_obj
|
|
31
|
+
|
|
32
|
+
elif isinstance(obj, list):
|
|
33
|
+
# Recursively process all items in the list
|
|
34
|
+
processed_list = [strip_ids_recursive(item) for item in obj]
|
|
35
|
+
# Remove None values from the list (if any were removed)
|
|
36
|
+
processed_list = [item for item in processed_list if item is not None]
|
|
37
|
+
return processed_list
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
# For primitive types (str, int, bool, None), return as-is
|
|
41
|
+
return obj
|
|
42
|
+
|
|
43
|
+
## General purpose functions
|
|
44
|
+
def exec_cli(command):
|
|
45
|
+
result = None
|
|
46
|
+
try:
|
|
47
|
+
response = subprocess.run(command, shell=True, text=True, capture_output=True)
|
|
48
|
+
if response.returncode != 0:
|
|
49
|
+
print(f"Command failed with return code {response.returncode}")
|
|
50
|
+
print(f"stderr: {response.stderr}")
|
|
51
|
+
return None
|
|
52
|
+
result = json.loads(response.stdout)
|
|
53
|
+
except json.JSONDecodeError as e:
|
|
54
|
+
print(f"Failed to parse JSON response: {e}")
|
|
55
|
+
print(f"stdout: {response.stdout}")
|
|
56
|
+
print(f"stderr: {response.stderr}")
|
|
57
|
+
return None
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"Failed to execute command: {e}")
|
|
60
|
+
return None
|
|
61
|
+
return result
|
|
62
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import csv
|
|
3
|
+
import subprocess
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import argparse
|
|
7
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
8
|
+
from graphql_client.api.call_api import ApiClient, CallApi
|
|
9
|
+
from graphql_client import Configuration
|
|
10
|
+
from graphql_client.api_client import ApiException
|
|
11
|
+
import catocli
|
|
12
|
+
|
|
13
|
+
# Configuration variables
|
|
14
|
+
DESTINATION_DIR = "config_data"
|
|
15
|
+
|
|
16
|
+
def strip_ids_recursive(data):
|
|
17
|
+
"""Recursively strip id attributes from data structure"""
|
|
18
|
+
if isinstance(data, dict):
|
|
19
|
+
return {k: strip_ids_recursive(v) for k, v in data.items() if k != 'id'}
|
|
20
|
+
elif isinstance(data, list):
|
|
21
|
+
return [strip_ids_recursive(item) for item in data]
|
|
22
|
+
else:
|
|
23
|
+
return data
|
|
24
|
+
|
|
25
|
+
def main():
|
|
26
|
+
# Parse command line arguments
|
|
27
|
+
parser = argparse.ArgumentParser(description='Export IFW rules to JSON')
|
|
28
|
+
parser.add_argument('-accountID', help='Account ID to export rules from', required=True)
|
|
29
|
+
parser.add_argument('--output-file-path', help='Full path including filename and extension for output file. If not specified, uses default: config_data/all_ifw_rules_and_sections_{account_id}.json')
|
|
30
|
+
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
ACCOUNT_ID = args.accountID
|
|
34
|
+
|
|
35
|
+
# Setup API client configuration
|
|
36
|
+
if "CATO_TOKEN" not in os.environ:
|
|
37
|
+
print("Missing authentication, please set the CATO_TOKEN environment variable with your api key.")
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
configuration = Configuration()
|
|
41
|
+
configuration.verify_ssl = False
|
|
42
|
+
configuration.api_key["x-api-key"] = os.getenv("CATO_TOKEN")
|
|
43
|
+
configuration.host = "{}".format(catocli.__cato_host__)
|
|
44
|
+
configuration.accountID = ACCOUNT_ID
|
|
45
|
+
|
|
46
|
+
# Set up output file path
|
|
47
|
+
if args.output_file_path:
|
|
48
|
+
# Use output file path if provided
|
|
49
|
+
output_file = args.output_file_path
|
|
50
|
+
destination_dir = os.path.dirname(output_file)
|
|
51
|
+
if args.verbose:
|
|
52
|
+
print(f"Using output file path: {output_file}")
|
|
53
|
+
else:
|
|
54
|
+
# Use default path and filename
|
|
55
|
+
destination_dir = 'config_data'
|
|
56
|
+
json_output_file = f"all_ifw_rules_and_sections_{ACCOUNT_ID}.json"
|
|
57
|
+
output_file = os.path.join(destination_dir, json_output_file)
|
|
58
|
+
if args.verbose:
|
|
59
|
+
print(f"Using default path: {output_file}")
|
|
60
|
+
|
|
61
|
+
# Create destination directory if it doesn't exist
|
|
62
|
+
if destination_dir and not os.path.exists(destination_dir):
|
|
63
|
+
if args.verbose:
|
|
64
|
+
print(f"Creating directory: {destination_dir}")
|
|
65
|
+
os.makedirs(destination_dir)
|
|
66
|
+
policyQuery = {
|
|
67
|
+
"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 } } } } }",
|
|
68
|
+
"variables": {
|
|
69
|
+
"accountId": ACCOUNT_ID
|
|
70
|
+
},
|
|
71
|
+
"operationName": "policy"
|
|
72
|
+
}
|
|
73
|
+
print(f"Retrieving all IFW rules and sections for account {ACCOUNT_ID}...")
|
|
74
|
+
|
|
75
|
+
# Create API client instance
|
|
76
|
+
instance = CallApi(ApiClient(configuration))
|
|
77
|
+
|
|
78
|
+
# Create params object for the API call
|
|
79
|
+
params = {
|
|
80
|
+
'v': False, # verbose mode
|
|
81
|
+
'f': 'json', # format
|
|
82
|
+
'p': False, # pretty print
|
|
83
|
+
't': False # test mode
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# Call the API directly
|
|
88
|
+
response = instance.call_api(policyQuery, params)
|
|
89
|
+
allIfwRules = response[0] if response else {}
|
|
90
|
+
|
|
91
|
+
if not allIfwRules or 'data' not in allIfwRules:
|
|
92
|
+
print("ERROR: Failed to retrieve data from API")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
except ApiException as e:
|
|
96
|
+
print(f"ERROR: API call failed - {e}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"ERROR: Unexpected error - {e}")
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
|
|
102
|
+
# First, preserve section IDs before stripping them
|
|
103
|
+
section_id_map = {}
|
|
104
|
+
sections_with_ids = allIfwRules['data']['policy']['internetFirewall']['policy']['sections']
|
|
105
|
+
for section_data in sections_with_ids:
|
|
106
|
+
section_id_map[section_data['section']['name']] = section_data['section']['id']
|
|
107
|
+
|
|
108
|
+
# Get the first section ID for section_to_start_after_id
|
|
109
|
+
section_to_start_after_id = None
|
|
110
|
+
if len(sections_with_ids) > 0:
|
|
111
|
+
section_to_start_after_id = sections_with_ids[0]['section']['id']
|
|
112
|
+
print(f"Excluding first section with system rule: {sections_with_ids[0]['section']['name']}")
|
|
113
|
+
|
|
114
|
+
## Processing data to strip id attributes
|
|
115
|
+
processed_data = strip_ids_recursive(allIfwRules)
|
|
116
|
+
|
|
117
|
+
## Filter out rules with properties[0]=="SYSTEM"
|
|
118
|
+
filtered_rules = []
|
|
119
|
+
for rule_data in processed_data['data']['policy']['internetFirewall']['policy']['rules']:
|
|
120
|
+
rule_properties = rule_data.get('properties', [])
|
|
121
|
+
# Skip rules where the first property is "SYSTEM"
|
|
122
|
+
if rule_properties and rule_properties[0] == "SYSTEM":
|
|
123
|
+
print(f"Excluding SYSTEM rule: {rule_data['rule']['name']}")
|
|
124
|
+
else:
|
|
125
|
+
filtered_rules.append(rule_data)
|
|
126
|
+
processed_data['data']['policy']['internetFirewall']['policy']['rules'] = filtered_rules
|
|
127
|
+
# rules = filtered_rules
|
|
128
|
+
|
|
129
|
+
# Add index_in_section to each rule
|
|
130
|
+
# Group rules by section and add index_in_section
|
|
131
|
+
section_counters = {}
|
|
132
|
+
for rule_data in processed_data['data']['policy']['internetFirewall']['policy']['rules']:
|
|
133
|
+
section_name = rule_data['rule']['section']['name']
|
|
134
|
+
if section_name not in section_counters:
|
|
135
|
+
section_counters[section_name] = 0
|
|
136
|
+
section_counters[section_name] += 1
|
|
137
|
+
rule_data['rule']['index_in_section'] = section_counters[section_name]
|
|
138
|
+
|
|
139
|
+
# Create rules_in_sections array
|
|
140
|
+
rules_in_sections = []
|
|
141
|
+
for rule_data in processed_data['data']['policy']['internetFirewall']['policy']['rules']:
|
|
142
|
+
rule_info = rule_data['rule']
|
|
143
|
+
rules_in_sections.append({
|
|
144
|
+
"index_in_section": rule_info['index_in_section'],
|
|
145
|
+
"section_name": rule_info['section']['name'],
|
|
146
|
+
"rule_name": rule_info['name']
|
|
147
|
+
})
|
|
148
|
+
rule_info.pop("index_in_section", None)
|
|
149
|
+
rule_info.pop("index", None)
|
|
150
|
+
rule_info["enabled"] = True
|
|
151
|
+
|
|
152
|
+
# Add rules_in_sections to the policy structure
|
|
153
|
+
processed_data['data']['policy']['internetFirewall']['policy']['rules_in_sections'] = rules_in_sections
|
|
154
|
+
|
|
155
|
+
# Reformat sections array to have index, section_id and section_name structure
|
|
156
|
+
# Exclude the first section from export
|
|
157
|
+
processed_sections = []
|
|
158
|
+
# Add first section containing reserved SYSTEM rules as section_to_start_after_id
|
|
159
|
+
for index, section_data in enumerate(processed_data['data']['policy']['internetFirewall']['policy']['sections']):
|
|
160
|
+
if index == 0:
|
|
161
|
+
# Skip the first section which contains reserved SYSTEM rules
|
|
162
|
+
continue
|
|
163
|
+
else:
|
|
164
|
+
processed_sections.append({
|
|
165
|
+
"section_index": index,
|
|
166
|
+
"section_name": section_data['section']['name']
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
# Add preserved section IDs and section_to_start_after_id
|
|
170
|
+
processed_data['data']['policy']['internetFirewall']['policy']['section_ids'] = section_id_map
|
|
171
|
+
if section_to_start_after_id:
|
|
172
|
+
processed_data['data']['policy']['internetFirewall']['policy']['section_to_start_after_id'] = section_to_start_after_id
|
|
173
|
+
|
|
174
|
+
if len(sections_with_ids) > 0:
|
|
175
|
+
print(f"Excluded first section: {sections_with_ids[0]['section']['name']} containing reserved SYSTEM rules")
|
|
176
|
+
|
|
177
|
+
# Replace the original sections array with the reformatted one
|
|
178
|
+
processed_data['data']['policy']['internetFirewall']['policy']['sections'] = processed_sections
|
|
179
|
+
|
|
180
|
+
# Write the processed data to the new file
|
|
181
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
182
|
+
json.dump(processed_data, f, indent=4, ensure_ascii=False)
|
|
183
|
+
print(f"Successfully created {output_file}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
main()
|
|
188
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import csv
|
|
3
|
+
import subprocess
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import argparse
|
|
7
|
+
import catolib
|
|
8
|
+
|
|
9
|
+
# Configuration variables
|
|
10
|
+
DESTINATION_DIR = "config_data"
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
# Parse command line arguments
|
|
14
|
+
parser = argparse.ArgumentParser(description='Export WAN rules to JSON')
|
|
15
|
+
parser.add_argument('account_id', help='Account ID to export rules from')
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
|
|
18
|
+
ACCOUNT_ID = args.account_id
|
|
19
|
+
JSON_OUTPUT_FILE = f"all_wf_rules_and_sections_{ACCOUNT_ID}.json"
|
|
20
|
+
# Create destination directory if it doesn't exist
|
|
21
|
+
if not os.path.exists(DESTINATION_DIR):
|
|
22
|
+
print(f"Creating directory: {DESTINATION_DIR}")
|
|
23
|
+
os.makedirs(DESTINATION_DIR)
|
|
24
|
+
|
|
25
|
+
# Set full file paths
|
|
26
|
+
output_file = os.path.join(DESTINATION_DIR, JSON_OUTPUT_FILE)
|
|
27
|
+
policyQuery = {
|
|
28
|
+
"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 } } } } }",
|
|
29
|
+
"variables": {
|
|
30
|
+
"accountId": ACCOUNT_ID
|
|
31
|
+
},
|
|
32
|
+
"operationName": "policy"
|
|
33
|
+
}
|
|
34
|
+
catocliCommand = "catocli raw '"+json.dumps(policyQuery)+"'"
|
|
35
|
+
print(f"Retrieving all WAN rules and sections for account {ACCOUNT_ID}...")
|
|
36
|
+
allWanRules = catolib.exec_cli(catocliCommand)
|
|
37
|
+
|
|
38
|
+
## Processing data to strip id attributes
|
|
39
|
+
processed_data = catolib.strip_ids_recursive(allWanRules)
|
|
40
|
+
|
|
41
|
+
## Preserving section IDs index by section name
|
|
42
|
+
section_id_map = {}
|
|
43
|
+
if 'data' in allWanRules and 'policy' in allWanRules['data'] and 'wanFirewall' in allWanRules['data']['policy']:
|
|
44
|
+
for section_data in allWanRules['data']['policy']['wanFirewall']['policy']['sections']:
|
|
45
|
+
section_id_map[section_data['section']['name']] = section_data['section']['id']
|
|
46
|
+
processed_data['data']['policy']['wanFirewall']['policy']['section_ids'] = section_id_map
|
|
47
|
+
|
|
48
|
+
## Filter out rules with properties[0]=="SYSTEM"
|
|
49
|
+
filtered_rules = []
|
|
50
|
+
for rule_data in processed_data['data']['policy']['wanFirewall']['policy']['rules']:
|
|
51
|
+
rule_properties = rule_data.get('properties', [])
|
|
52
|
+
# Skip rules where the first property is "SYSTEM"
|
|
53
|
+
if rule_properties and rule_properties[0] == "SYSTEM":
|
|
54
|
+
print(f"Excluding SYSTEM rule: {rule_data['rule']['name']}")
|
|
55
|
+
else:
|
|
56
|
+
filtered_rules.append(rule_data)
|
|
57
|
+
processed_data['data']['policy']['wanFirewall']['policy']['rules'] = filtered_rules
|
|
58
|
+
# rules = filtered_rules
|
|
59
|
+
|
|
60
|
+
# Add index_in_section to each rule
|
|
61
|
+
# Group rules by section and add index_in_section
|
|
62
|
+
section_counters = {}
|
|
63
|
+
for rule_data in processed_data['data']['policy']['wanFirewall']['policy']['rules']:
|
|
64
|
+
section_name = rule_data['rule']['section']['name']
|
|
65
|
+
if section_name not in section_counters:
|
|
66
|
+
section_counters[section_name] = 0
|
|
67
|
+
section_counters[section_name] += 1
|
|
68
|
+
rule_data['rule']['index_in_section'] = section_counters[section_name]
|
|
69
|
+
|
|
70
|
+
# Create rules_in_sections array
|
|
71
|
+
rules_in_sections = []
|
|
72
|
+
for rule_data in processed_data['data']['policy']['wanFirewall']['policy']['rules']:
|
|
73
|
+
rule_info = rule_data['rule']
|
|
74
|
+
rules_in_sections.append({
|
|
75
|
+
"index_in_section": rule_info['index_in_section'],
|
|
76
|
+
"section_name": rule_info['section']['name'],
|
|
77
|
+
"rule_name": rule_info['name']
|
|
78
|
+
})
|
|
79
|
+
rule_info.pop("index_in_section", None)
|
|
80
|
+
rule_info.pop("index", None)
|
|
81
|
+
rule_info["enabled"] = True
|
|
82
|
+
|
|
83
|
+
# Add rules_in_sections to the policy structure
|
|
84
|
+
processed_data['data']['policy']['wanFirewall']['policy']['rules_in_sections'] = rules_in_sections
|
|
85
|
+
|
|
86
|
+
# Reformat sections array to have index, section_id and section_name structure
|
|
87
|
+
# Exclude the first section from export, excluding first section
|
|
88
|
+
|
|
89
|
+
processed_sections = []
|
|
90
|
+
# Add first section containing reserved SYSTEM rules as section_to_start_after_id
|
|
91
|
+
for index, section_data in enumerate(processed_data['data']['policy']['wanFirewall']['policy']['sections']):
|
|
92
|
+
processed_sections.append({
|
|
93
|
+
"section_index": index,
|
|
94
|
+
"section_name": section_data['section']['name']
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
if len(processed_data['data']['policy']['wanFirewall']['policy']['sections']) > 0:
|
|
98
|
+
print(f"Excluded first section: {processed_data['data']['policy']['wanFirewall']['policy']['sections'][0]['section']['name']} containing reserved SYSTEM rules")
|
|
99
|
+
|
|
100
|
+
# Replace the original sections array with the reformatted one
|
|
101
|
+
processed_data['data']['policy']['wanFirewall']['policy']['sections'] = processed_sections
|
|
102
|
+
|
|
103
|
+
# Write the processed data to the new file
|
|
104
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
105
|
+
json.dump(processed_data, f, indent=4, ensure_ascii=False)
|
|
106
|
+
print(f"Successfully created {output_file}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|
|
111
|
+
|