From b08ece244c02b3a37fc37f5b7c1ce5fba67c9e6b Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 25 Mar 2026 15:56:49 +0100 Subject: [PATCH 01/63] feat: added standard layer --- .../fw_modules/checkpointR8x/fwcommon.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index 2d8f2fa3c1..e5831ec607 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -15,6 +15,8 @@ from models.fwconfig_normalized import FwConfigNormalized from models.fwconfigmanagerlist import FwConfigManager from models.import_state import ImportState +from models.rulebase import Rulebase +from models.rulebase_link import RulebaseLinkUidBased from utils.conversion_utils import convert_list_to_dict @@ -94,6 +96,7 @@ def get_config( FWOLogger.info("completed getting config") return 0, normalized_config # we already have a native config (from file import) + add_standard_rulebase(config_in) return 0, config_in @@ -187,9 +190,34 @@ def normalize_config( ) config_in.ManagerSet.append(manager) + add_standard_rulebase(config_in) return config_in +def add_standard_rulebase(config_in: FwConfigManagerListController) -> None: + for manager in config_in.ManagerSet: + for config in manager.configs: + if not any(rb.name == "Standard Rulebase" for rb in config.rulebases): + rb = Rulebase(name="Standard Rulebase", uid="Standard", mgm_uid=manager.manager_uid, rules={}) + config.rulebases.append(rb) + for gw in config.gateways: + gw.RulebaseLinks[0].from_rulebase_uid = rb.uid + gw.RulebaseLinks[0].from_rule_uid = None + gw.RulebaseLinks[0].is_initial = False + gw.RulebaseLinks.insert( + 0, + RulebaseLinkUidBased( + from_rule_uid=None, + from_rulebase_uid=None, + to_rulebase_uid=rb.uid, + link_type="i", + is_global=False, + is_initial=True, + is_section=False, + ), + ) + + def ensure_native_domains(native_config: dict[str, Any], import_state: ImportState) -> None: if "domains" in native_config: return From 64da640af84b5b8fe94e66d80cfece39e3f30413 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 25 Mar 2026 16:01:14 +0100 Subject: [PATCH 02/63] feat: stuff --- .../files/importer/fw_modules/checkpointR8x/fwcommon.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index e5831ec607..f5c0278443 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -197,8 +197,8 @@ def normalize_config( def add_standard_rulebase(config_in: FwConfigManagerListController) -> None: for manager in config_in.ManagerSet: for config in manager.configs: - if not any(rb.name == "Standard Rulebase" for rb in config.rulebases): - rb = Rulebase(name="Standard Rulebase", uid="Standard", mgm_uid=manager.manager_uid, rules={}) + if not any(rb.name == "Standard" for rb in config.rulebases): + rb = Rulebase(name="Standard", uid="Standard", mgm_uid=manager.manager_uid, rules={}) config.rulebases.append(rb) for gw in config.gateways: gw.RulebaseLinks[0].from_rulebase_uid = rb.uid @@ -210,7 +210,7 @@ def add_standard_rulebase(config_in: FwConfigManagerListController) -> None: from_rule_uid=None, from_rulebase_uid=None, to_rulebase_uid=rb.uid, - link_type="i", + link_type="ordered", is_global=False, is_initial=True, is_section=False, From af6860073b488e0984dc7c5ba28b0bf389e259bc Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Wed, 25 Mar 2026 16:06:08 +0100 Subject: [PATCH 03/63] chore: Add TODO --- roles/importer/files/importer/fwo_config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fwo_config.py b/roles/importer/files/importer/fwo_config.py index e9720ca8d5..ab4b5f7e29 100644 --- a/roles/importer/files/importer/fwo_config.py +++ b/roles/importer/files/importer/fwo_config.py @@ -5,7 +5,9 @@ from fwo_log import FWOLogger -def read_config(fwo_config_filename: str = "/etc/fworch/fworch.json") -> dict[str, str | int | None]: +def read_config( + fwo_config_filename: str = "/Users/lennart/Dev/wws/firewall-orchestrator-config/fworch.json", +) -> dict[str, str | int | None]: try: # read fwo config (API URLs) with open(fwo_config_filename) as fwo_config: From 5ffdaff39a286b08d9a2104c5e70da099cf41444 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Wed, 25 Mar 2026 18:44:15 +0100 Subject: [PATCH 04/63] fix: Policy registration --- .../fw_modules/checkpointR8x/cp_getter.py | 9 ++ .../fw_modules/checkpointR8x/fwcommon.py | 90 +++++++++---------- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py index 39dcb15e93..e192919f7e 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py @@ -342,6 +342,7 @@ def get_rulebases( native_config_domain: dict[str, Any] | None, device_config: dict[str, Any] | None, policy_rulebases_uid_list: list[str], + policy_structure: dict[str, Any], is_global: bool = False, access_type: str = "access", rulebase_uid: str | None = None, @@ -363,6 +364,11 @@ def get_rulebases( else: FWOLogger.error('access_type is neither "access" nor "nat", but ' + access_type) + if not any(rb["uid"] == policy_structure["uid"] for rb in native_config_domain[native_config_rulebase_key]): + native_config_domain[native_config_rulebase_key].append( + {"uid": policy_structure["uid"], "name": policy_structure["name"], "chunks": []} + ) + # get uid of rulebase if rulebase_uid is None: if rulebase_name is not None: @@ -401,6 +407,7 @@ def get_rulebases( show_params_rules, is_global, policy_rulebases_uid_list, + policy_structure=policy_structure, ) @@ -524,6 +531,7 @@ def get_inline_layers_recursively( show_params_rules: dict[str, Any], is_global: bool, policy_rulebases_uid_list: list[str], + policy_structure: dict[str, Any], ) -> list[str]: """ Takes current_rulebase, splits sections into sub-rulebases and searches for layerguards to fetch @@ -563,6 +571,7 @@ def get_inline_layers_recursively( is_global=is_global, access_type="access", rulebase_uid=rule["inline-layer"], + policy_structure=policy_structure, ) return policy_rulebases_uid_list diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index f5c0278443..e84d291c5a 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -4,7 +4,7 @@ import fwo_const import fwo_globals -from fw_modules.checkpointR8x import cp_const, cp_gateway, cp_getter, cp_network, cp_rule, cp_service +from fw_modules.checkpointR8x import cp_const, cp_gateway, cp_getter, cp_nat, cp_network, cp_rule, cp_service from fwo_base import ConfigAction from fwo_exceptions import FwLoginFailedError, FwoImporterError, ImportInterruptionError from fwo_log import FWOLogger @@ -15,8 +15,6 @@ from models.fwconfig_normalized import FwConfigNormalized from models.fwconfigmanagerlist import FwConfigManager from models.import_state import ImportState -from models.rulebase import Rulebase -from models.rulebase_link import RulebaseLinkUidBased from utils.conversion_utils import convert_list_to_dict @@ -96,7 +94,6 @@ def get_config( FWOLogger.info("completed getting config") return 0, normalized_config # we already have a native config (from file import) - add_standard_rulebase(config_in) return 0, config_in @@ -190,34 +187,9 @@ def normalize_config( ) config_in.ManagerSet.append(manager) - add_standard_rulebase(config_in) return config_in -def add_standard_rulebase(config_in: FwConfigManagerListController) -> None: - for manager in config_in.ManagerSet: - for config in manager.configs: - if not any(rb.name == "Standard" for rb in config.rulebases): - rb = Rulebase(name="Standard", uid="Standard", mgm_uid=manager.manager_uid, rules={}) - config.rulebases.append(rb) - for gw in config.gateways: - gw.RulebaseLinks[0].from_rulebase_uid = rb.uid - gw.RulebaseLinks[0].from_rule_uid = None - gw.RulebaseLinks[0].is_initial = False - gw.RulebaseLinks.insert( - 0, - RulebaseLinkUidBased( - from_rule_uid=None, - from_rulebase_uid=None, - to_rulebase_uid=rb.uid, - link_type="ordered", - is_global=False, - is_initial=True, - is_section=False, - ), - ) - - def ensure_native_domains(native_config: dict[str, Any], import_state: ImportState) -> None: if "domains" in native_config: return @@ -257,6 +229,7 @@ def normalize_single_manager_config( cp_network.normalize_time_objects(native_config, normalized_config_dict) FWOLogger.info("completed normalizing time objects") cp_gateway.normalize_gateways(native_config, import_state, normalized_config_dict) + cp_nat.normalize_nat_rules(native_config, import_state, normalized_config_dict) cp_rule.normalize_rulebases( native_config, native_config_global, @@ -407,8 +380,12 @@ def process_devices( cp_manager_api_base_url, ) else: - define_initial_rulebase(device_config, ordered_layer_uids, is_global=False) + define_initial_rulebase_links(device_config, ordered_layer_uids, is_global=False) + policy_structure_dict = next( + (policy for policy in policy_structure if policy["uid"] == ordered_layer_uids[0]), + {"uid": ordered_layer_uids[0]}, + ) add_ordered_layers_to_native_config( ordered_layer_uids, get_rules_params(import_state), @@ -418,9 +395,10 @@ def process_devices( device_config, is_global=False, global_ordered_layer_count=global_ordered_layer_count, + policy_structure=policy_structure_dict, ) - handle_nat_rules(device, native_config_domain, sid, import_state) + handle_nat_rules(native_config_domain, sid, import_state) native_config_domain["gateways"].append(device_config) @@ -473,6 +451,7 @@ def handle_global_rulebase_links( device_config, is_global=True, global_ordered_layer_count=global_ordered_layer_count, + policy_structure=global_policy, ) define_global_rulebase_link( device_config, @@ -497,7 +476,7 @@ def define_global_rulebase_link( """ Links initial and placeholder rule for global rulebases """ - define_initial_rulebase(device_config, global_ordered_layer_uids, is_global=True) + define_initial_rulebase_links(device_config, global_ordered_layer_uids, is_global=True) # parse global rulebases, find place-holders and link local rulebases placeholder_link_index = 0 @@ -528,17 +507,28 @@ def define_global_rulebase_link( placeholder_link_index += 1 -def define_initial_rulebase(device_config: dict[str, Any], ordered_layer_uids: list[str], is_global: bool): - device_config["rulebase_links"].append( - { - "from_rulebase_uid": None, - "from_rule_uid": None, - "to_rulebase_uid": ordered_layer_uids[0], - "type": "ordered", - "is_global": is_global, - "is_initial": True, - "is_section": False, - } +def define_initial_rulebase_links(device_config: dict[str, Any], ordered_layer_uids: list[str], is_global: bool): + device_config["rulebase_links"].extend( + [ + { + "from_rulebase_uid": None, + "from_rule_uid": None, + "to_rulebase_uid": ordered_layer_uids[0], + "type": "ordered", + "is_global": is_global, + "is_initial": True, + "is_section": False, + }, + { + "from_rulebase_uid": ordered_layer_uids[0], + "from_rule_uid": None, + "to_rulebase_uid": ordered_layer_uids[1], + "type": "ordered", + "is_global": is_global, + "is_initial": False, + "is_section": False, + }, + ] ) @@ -551,15 +541,15 @@ def get_rules_params(import_state: ImportState) -> dict[str, Any]: } -def handle_nat_rules(device: dict[str, Any], native_config_domain: dict[str, Any], sid: str, import_state: ImportState): - if device.get("package_name"): +def handle_nat_rules(native_config_domain: dict[str, Any], sid: str, import_state: ImportState): + if "rulebases" in native_config_domain and len(native_config_domain["rulebases"]) > 0: + first_rulebase_name = native_config_domain["rulebases"][0]["name"] show_params_rules: dict[str, Any] = { "limit": import_state.fwo_config.api_fetch_size, "use-object-dictionary": cp_const.use_object_dictionary, - "details-level": "standard", - "package": device["package_name"], + "package": first_rulebase_name, } - FWOLogger.debug(f"Getting NAT rules for package: {device['package_name']}", 4) + FWOLogger.debug(f"Getting NAT rules for package: {first_rulebase_name}", 4) nat_rules = cp_getter.get_nat_rules_from_api_as_dict( import_state.mgm_details.build_fw_api_string(), sid, @@ -583,6 +573,7 @@ def add_ordered_layers_to_native_config( device_config: dict[str, Any], is_global: bool, global_ordered_layer_count: int, + policy_structure: dict[str, Any], ) -> list[str]: """ Fetches ordered layers and links them @@ -601,6 +592,7 @@ def add_ordered_layers_to_native_config( is_global=is_global, access_type="access", rulebase_uid=ordered_layer_uid, + policy_structure=policy_structure, ) # link to next ordered layer @@ -632,6 +624,8 @@ def get_ordered_layer_uids( ordered_layer_uids: list[str] = [] for policy in policy_structure: found_target_in_policy = False + if "uid" in policy: + ordered_layer_uids.extend([policy["uid"]]) for target in policy["targets"]: if target["uid"] == device_config["uid"] or target["uid"] == "all": found_target_in_policy = True From 10ecb5b16b4e8fe3ca702088d7aca0e1c7efd650 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Wed, 25 Mar 2026 18:44:20 +0100 Subject: [PATCH 05/63] wip: Nat --- .../fw_modules/checkpointR8x/cp_nat.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py new file mode 100644 index 0000000000..ea11d4c640 --- /dev/null +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -0,0 +1,112 @@ +import json +from typing import Any + +from fw_modules.checkpointR8x.cp_rule import check_and_add_section_header, parse_single_rule +from fwo_log import FWOLogger +from models.import_state import ImportState +from models.rulebase import Rulebase + + +def normalize_nat_rules(native_config: dict[str, Any], import_state: ImportState, normalized_config: dict[str, Any]): + native_nat_rulebases = native_config.get("nat_rulebases", []) + if not native_nat_rulebases: + return + for nat_rulebase in native_nat_rulebases: + if "nat_rule_chunks" in nat_rulebase: + # parse chunks + pass + else: + # parse rulebase + pass + + +def parse_nat_rulebase( + src_rulebase: dict[str, Any], + target_rulebase: Rulebase, + layer_name: str, + import_id: str, + section_header_uids: set[str], + parent_uid: str, + gateway: dict[str, Any], + policy_structure: list[dict[str, Any]], + debug_level: int = 0, + recursion_level: int = 1, +): + if recursion_level > 1000000: + raise Exception("ImportRecursionLimitReached(parse_nat_rulebase_json) from None") + + if "nat_rule_chunks" in src_rulebase: + for chunk in src_rulebase["nat_rule_chunks"]: + if "rulebase" in chunk: + for rules_chunk in chunk["rulebase"]: + parse_nat_rulebase( + rules_chunk, + target_rulebase, + layer_name, + import_id, + section_header_uids, + parent_uid, + gateway, + policy_structure, + debug_level=debug_level, + recursion_level=recursion_level + 1, + ) + else: + FWOLogger.warning(f"parse_rule: found no rulebase in chunk:\n{json.dumps(chunk, indent=2)}") + else: + if "rulebase" in src_rulebase: + check_and_add_section_header(src_rulebase, target_rulebase, layer_name, import_id, section_header_uids) + + for rule in src_rulebase["rulebase"]: + (rule_match, rule_xlate) = parse_nat_rule_transform(rule) + parse_single_rule(rule_match, target_rulebase, layer_name, parent_uid, gateway, policy_structure) + parse_single_rule( # do not increase rule_num here + rule_xlate, target_rulebase, layer_name, parent_uid, gateway, policy_structure + ) + + if "rule-number" in src_rulebase: # rulebase is just a single rule (xlate rules do not count) + (rule_match, rule_xlate) = parse_nat_rule_transform(src_rulebase) + parse_single_rule(rule_match, target_rulebase, layer_name, parent_uid, gateway, policy_structure) + parse_single_rule( # do not increase rule_num here (xlate rules do not count) + rule_xlate, target_rulebase, layer_name, parent_uid, gateway, policy_structure + ) + + +def parse_nat_rule_transform(xlate_rule_in: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: + # TODO: cleanup certain fields (install-on, ....) + rule_match = { + "uid": xlate_rule_in["uid"], + "source": [xlate_rule_in["original-source"]], + "destination": [xlate_rule_in["original-destination"]], + "service": [xlate_rule_in["original-service"]], + "action": {"name": "Drop"}, + "track": {"type": {"name": "None"}}, + "type": "nat", + "rule-number": 0, + "source-negate": False, + "destination-negate": False, + "service-negate": False, + "install-on": [{"name": "Policy Targets"}], + "time": [{"name": "Any"}], + "enabled": xlate_rule_in["enabled"], + "comments": xlate_rule_in["comments"], + "rule_type": "access", + } + rule_xlate = { + "uid": xlate_rule_in["uid"], + "source": [xlate_rule_in["translated-source"]], + "destination": [xlate_rule_in["translated-destination"]], + "service": [xlate_rule_in["translated-service"]], + "action": {"name": "Drop"}, + "track": {"type": {"name": "None"}}, + "type": "nat", + "rule-number": 0, + "enabled": True, + "source-negate": False, + "destination-negate": False, + "service-negate": False, + "install-on": [{"name": "Policy Targets"}], + "time": [{"name": "Any"}], + "rule_type": "nat", + } + return (rule_match, rule_xlate) From a3d79be8c651f41ec92777f636fe1102623c9730 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Wed, 25 Mar 2026 19:01:51 +0100 Subject: [PATCH 06/63] fix: duplicate rulebase --- .../fw_modules/checkpointR8x/fwcommon.py | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index e84d291c5a..b306f17d06 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -508,27 +508,16 @@ def define_global_rulebase_link( def define_initial_rulebase_links(device_config: dict[str, Any], ordered_layer_uids: list[str], is_global: bool): - device_config["rulebase_links"].extend( - [ - { - "from_rulebase_uid": None, - "from_rule_uid": None, - "to_rulebase_uid": ordered_layer_uids[0], - "type": "ordered", - "is_global": is_global, - "is_initial": True, - "is_section": False, - }, - { - "from_rulebase_uid": ordered_layer_uids[0], - "from_rule_uid": None, - "to_rulebase_uid": ordered_layer_uids[1], - "type": "ordered", - "is_global": is_global, - "is_initial": False, - "is_section": False, - }, - ] + device_config["rulebase_links"].append( + { + "from_rulebase_uid": None, + "from_rule_uid": None, + "to_rulebase_uid": ordered_layer_uids[0], + "type": "ordered", + "is_global": is_global, + "is_initial": True, + "is_section": False, + } ) From 032920c8db4c77b8211b2d381f95378bf0a707ee Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Wed, 25 Mar 2026 20:40:42 +0100 Subject: [PATCH 07/63] wip: Broken with two policies --- .../fw_modules/checkpointR8x/cp_rule.py | 4 +- .../fw_modules/checkpointR8x/fwcommon.py | 70 ++++++++++++++----- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py index 755d801cea..5abf2e03d0 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py @@ -66,7 +66,9 @@ def normalize_rulebases_for_each_link_destination( normalized_config_global: dict[str, Any], ): for rulebase_link in gateway["rulebase_links"]: - if rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "": + if ( + rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "" + ) and rulebase_link["to_rulebase_uid"] not in [policy["uid"] for policy in native_config.get("policies", [])]: rulebase_to_parse, is_section, is_placeholder = find_rulebase_to_parse( native_config["rulebases"], rulebase_link["to_rulebase_uid"] ) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index b306f17d06..ffc3a647fe 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -380,12 +380,17 @@ def process_devices( cp_manager_api_base_url, ) else: - define_initial_rulebase_links(device_config, ordered_layer_uids, is_global=False) + define_initial_rulebase_links(device_config, ordered_layer_uids, policy_structure, is_global=False) policy_structure_dict = next( - (policy for policy in policy_structure if policy["uid"] == ordered_layer_uids[0]), - {"uid": ordered_layer_uids[0]}, + ( + policy + for policy in policy_structure + if any(access_layer["uid"] in ordered_layer_uids for access_layer in policy["access-layers"]) + ), + {"uid": ""}, ) + add_ordered_layers_to_native_config( ordered_layer_uids, get_rules_params(import_state), @@ -459,6 +464,7 @@ def handle_global_rulebase_links( ordered_layer_uids, native_config_global_domain, global_policy_rulebases_uid_list, + global_policy_structure, ) return global_ordered_layer_count @@ -472,11 +478,12 @@ def define_global_rulebase_link( ordered_layer_uids: list[str], native_config_global_domain: dict[str, Any], global_policy_rulebases_uid_list: list[str], + policy_structure: list[dict[str, Any]], ): """ Links initial and placeholder rule for global rulebases """ - define_initial_rulebase_links(device_config, global_ordered_layer_uids, is_global=True) + define_initial_rulebase_links(device_config, global_ordered_layer_uids, policy_structure, is_global=True) # parse global rulebases, find place-holders and link local rulebases placeholder_link_index = 0 @@ -507,18 +514,47 @@ def define_global_rulebase_link( placeholder_link_index += 1 -def define_initial_rulebase_links(device_config: dict[str, Any], ordered_layer_uids: list[str], is_global: bool): - device_config["rulebase_links"].append( - { - "from_rulebase_uid": None, - "from_rule_uid": None, - "to_rulebase_uid": ordered_layer_uids[0], - "type": "ordered", - "is_global": is_global, - "is_initial": True, - "is_section": False, - } +def define_initial_rulebase_links( + device_config: dict[str, Any], + ordered_layer_uids: list[str], + policy_structures: list[dict[str, Any]], + is_global: bool, +): + + for policy in policy_structures: + device_config["rulebase_links"].append( + { + "from_rulebase_uid": None, + "from_rule_uid": None, + "to_rulebase_uid": policy["uid"], + "type": "ordered", + "is_global": is_global, + "is_initial": True, + "is_section": False, + } + ) + + """ policy_structure = next( + (policy for policy in policy_structures if policy["uid"] == ordered_layer_uids[0]), + {"uid": ordered_layer_uids[0]}, ) + access_layers_uuids = [access_layer["uid"] for access_layer in policy_structure.get("access-layers", [])] # pyright: ignore[reportArgumentType] + + contains_any_uuid = any(uid in access_layers_uuids for uid in ordered_layer_uids) + if contains_any_uuid: + device_config["rulebase_links"].append( + { + "from_rulebase_uid": None, + "from_rule_uid": None, + "to_rulebase_uid": ordered_layer_uids[0], + "type": "ordered", + "is_global": is_global, + "is_initial": True, + "is_section": False, + } + ) + else: + del ordered_layer_uids[0] """ def get_rules_params(import_state: ImportState) -> dict[str, Any]: @@ -613,8 +649,8 @@ def get_ordered_layer_uids( ordered_layer_uids: list[str] = [] for policy in policy_structure: found_target_in_policy = False - if "uid" in policy: - ordered_layer_uids.extend([policy["uid"]]) + # if "uid" in policy: + # ordered_layer_uids.extend([policy["uid"]]) for target in policy["targets"]: if target["uid"] == device_config["uid"] or target["uid"] == "all": found_target_in_policy = True From d46c0a4f4c8baa15cdd3279c0d76e9cbab862589 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Fri, 27 Mar 2026 15:23:13 +0100 Subject: [PATCH 08/63] Revert "chore: Add TODO" This reverts commit af6860073b488e0984dc7c5ba28b0bf389e259bc. --- roles/importer/files/importer/fwo_config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/roles/importer/files/importer/fwo_config.py b/roles/importer/files/importer/fwo_config.py index ab4b5f7e29..e9720ca8d5 100644 --- a/roles/importer/files/importer/fwo_config.py +++ b/roles/importer/files/importer/fwo_config.py @@ -5,9 +5,7 @@ from fwo_log import FWOLogger -def read_config( - fwo_config_filename: str = "/Users/lennart/Dev/wws/firewall-orchestrator-config/fworch.json", -) -> dict[str, str | int | None]: +def read_config(fwo_config_filename: str = "/etc/fworch/fworch.json") -> dict[str, str | int | None]: try: # read fwo config (API URLs) with open(fwo_config_filename) as fwo_config: From 32029e6b2416fd7e72e6657f5da6a18400fbe4af Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Fri, 27 Mar 2026 17:28:40 +0100 Subject: [PATCH 09/63] fix: Checkpoint Policy Import --- .../fw_modules/checkpointR8x/cp_getter.py | 5 -- .../fw_modules/checkpointR8x/cp_rule.py | 8 ++- .../fw_modules/checkpointR8x/fwcommon.py | 68 ++++++++----------- 3 files changed, 35 insertions(+), 46 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py index e192919f7e..86197d6133 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py @@ -364,11 +364,6 @@ def get_rulebases( else: FWOLogger.error('access_type is neither "access" nor "nat", but ' + access_type) - if not any(rb["uid"] == policy_structure["uid"] for rb in native_config_domain[native_config_rulebase_key]): - native_config_domain[native_config_rulebase_key].append( - {"uid": policy_structure["uid"], "name": policy_structure["name"], "chunks": []} - ) - # get uid of rulebase if rulebase_uid is None: if rulebase_name is not None: diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py index 5abf2e03d0..2ff24d6ba4 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py @@ -66,9 +66,7 @@ def normalize_rulebases_for_each_link_destination( normalized_config_global: dict[str, Any], ): for rulebase_link in gateway["rulebase_links"]: - if ( - rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "" - ) and rulebase_link["to_rulebase_uid"] not in [policy["uid"] for policy in native_config.get("policies", [])]: + if rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "": rulebase_to_parse, is_section, is_placeholder = find_rulebase_to_parse( native_config["rulebases"], rulebase_link["to_rulebase_uid"] ) @@ -187,6 +185,10 @@ def parse_rulebase_chunk( policy_structure: list[dict[str, Any]], ): for chunk in rulebase_to_parse["chunks"]: + if "rulebase" not in chunk: + FWOLogger.debug("found unparsable rulebase chunk: " + str(chunk), 9) + continue + for rule in chunk["rulebase"]: if "rule-number" in rule: parse_single_rule(rule, normalized_rulebase, normalized_rulebase.uid, None, gateway, policy_structure) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index ffc3a647fe..1c70f5b6c6 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -380,15 +380,16 @@ def process_devices( cp_manager_api_base_url, ) else: - define_initial_rulebase_links(device_config, ordered_layer_uids, policy_structure, is_global=False) + define_initial_rulebase_links( + device_config, + policy_structure, + is_global=False, + native_config_domain=native_config_global_domain, + ) policy_structure_dict = next( - ( - policy - for policy in policy_structure - if any(access_layer["uid"] in ordered_layer_uids for access_layer in policy["access-layers"]) - ), - {"uid": ""}, + (policy for policy in policy_structure if policy["uid"] == ordered_layer_uids[0]), + {"uid": ordered_layer_uids[0]}, ) add_ordered_layers_to_native_config( @@ -460,7 +461,6 @@ def handle_global_rulebase_links( ) define_global_rulebase_link( device_config, - global_ordered_layer_uids, ordered_layer_uids, native_config_global_domain, global_policy_rulebases_uid_list, @@ -474,7 +474,6 @@ def handle_global_rulebase_links( def define_global_rulebase_link( device_config: dict[str, Any], - global_ordered_layer_uids: list[str], ordered_layer_uids: list[str], native_config_global_domain: dict[str, Any], global_policy_rulebases_uid_list: list[str], @@ -483,7 +482,12 @@ def define_global_rulebase_link( """ Links initial and placeholder rule for global rulebases """ - define_initial_rulebase_links(device_config, global_ordered_layer_uids, policy_structure, is_global=True) + define_initial_rulebase_links( + device_config, + policy_structure, + is_global=True, + native_config_domain=native_config_global_domain, + ) # parse global rulebases, find place-holders and link local rulebases placeholder_link_index = 0 @@ -516,45 +520,27 @@ def define_global_rulebase_link( def define_initial_rulebase_links( device_config: dict[str, Any], - ordered_layer_uids: list[str], policy_structures: list[dict[str, Any]], is_global: bool, + native_config_domain: dict[str, Any] | None, ): - + if native_config_domain is None: + native_config_domain = {"rulebases": []} for policy in policy_structures: - device_config["rulebase_links"].append( - { - "from_rulebase_uid": None, - "from_rule_uid": None, - "to_rulebase_uid": policy["uid"], - "type": "ordered", - "is_global": is_global, - "is_initial": True, - "is_section": False, - } - ) + if not any(rb["uid"] == policy["uid"] for rb in native_config_domain["rulebases"]): + native_config_domain["rulebases"].append({"uid": policy["uid"], "name": policy["name"], "chunks": []}) - """ policy_structure = next( - (policy for policy in policy_structures if policy["uid"] == ordered_layer_uids[0]), - {"uid": ordered_layer_uids[0]}, - ) - access_layers_uuids = [access_layer["uid"] for access_layer in policy_structure.get("access-layers", [])] # pyright: ignore[reportArgumentType] - - contains_any_uuid = any(uid in access_layers_uuids for uid in ordered_layer_uids) - if contains_any_uuid: device_config["rulebase_links"].append( { "from_rulebase_uid": None, "from_rule_uid": None, - "to_rulebase_uid": ordered_layer_uids[0], + "to_rulebase_uid": policy["uid"], "type": "ordered", "is_global": is_global, "is_initial": True, "is_section": False, } ) - else: - del ordered_layer_uids[0] """ def get_rules_params(import_state: ImportState) -> dict[str, Any]: @@ -567,6 +553,7 @@ def get_rules_params(import_state: ImportState) -> dict[str, Any]: def handle_nat_rules(native_config_domain: dict[str, Any], sid: str, import_state: ImportState): + return if "rulebases" in native_config_domain and len(native_config_domain["rulebases"]) > 0: first_rulebase_name = native_config_domain["rulebases"][0]["name"] show_params_rules: dict[str, Any] = { @@ -622,8 +609,13 @@ def add_ordered_layers_to_native_config( # link to next ordered layer # in case of mds: domain ordered layers are linked once there is no global ordered layer counterpart - if (is_global or ordered_layer_index >= global_ordered_layer_count - 1) and ( - ordered_layer_index < len(ordered_layer_uids) - 1 + if ( + (is_global or ordered_layer_index >= global_ordered_layer_count - 1) + and (ordered_layer_index < len(ordered_layer_uids) - 1) + and not any( + link["to_rulebase_uid"] == ordered_layer_uids[ordered_layer_index + 1] + for link in device_config["rulebase_links"] + ) ): device_config["rulebase_links"].append( { @@ -649,8 +641,8 @@ def get_ordered_layer_uids( ordered_layer_uids: list[str] = [] for policy in policy_structure: found_target_in_policy = False - # if "uid" in policy: - # ordered_layer_uids.extend([policy["uid"]]) + if "uid" in policy: + ordered_layer_uids.extend([policy["uid"]]) for target in policy["targets"]: if target["uid"] == device_config["uid"] or target["uid"] == "all": found_target_in_policy = True From eb60980730dceba841668dbcbd03f834c343bc33 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Fri, 27 Mar 2026 17:37:53 +0100 Subject: [PATCH 10/63] feat: Get NAT rulebases --- .../fw_modules/checkpointR8x/fwcommon.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index 1c70f5b6c6..cd0d8c2f00 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -404,7 +404,7 @@ def process_devices( policy_structure=policy_structure_dict, ) - handle_nat_rules(native_config_domain, sid, import_state) + handle_nat_rules(native_config_domain, sid, import_state, policy_structure) native_config_domain["gateways"].append(device_config) @@ -552,16 +552,16 @@ def get_rules_params(import_state: ImportState) -> dict[str, Any]: } -def handle_nat_rules(native_config_domain: dict[str, Any], sid: str, import_state: ImportState): - return - if "rulebases" in native_config_domain and len(native_config_domain["rulebases"]) > 0: - first_rulebase_name = native_config_domain["rulebases"][0]["name"] +def handle_nat_rules( + native_config_domain: dict[str, Any], sid: str, import_state: ImportState, policy_structure: list[dict[str, Any]] +): + for policy in policy_structure: show_params_rules: dict[str, Any] = { "limit": import_state.fwo_config.api_fetch_size, "use-object-dictionary": cp_const.use_object_dictionary, - "package": first_rulebase_name, + "package": policy["name"], } - FWOLogger.debug(f"Getting NAT rules for package: {first_rulebase_name}", 4) + FWOLogger.debug(f"Getting NAT rules for package: {policy['name']}", 4) nat_rules = cp_getter.get_nat_rules_from_api_as_dict( import_state.mgm_details.build_fw_api_string(), sid, @@ -572,8 +572,6 @@ def handle_nat_rules(native_config_domain: dict[str, Any], sid: str, import_stat native_config_domain["nat_rulebases"].append(nat_rules) else: native_config_domain["nat_rulebases"].append({"nat_rule_chunks": []}) - else: - native_config_domain["nat_rulebases"].append({"nat_rule_chunks": []}) def add_ordered_layers_to_native_config( From a4dedce47b72d17bd82c69f44011024affe0249a Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Fri, 27 Mar 2026 19:17:37 +0100 Subject: [PATCH 11/63] wip: NAT import --- .../fw_modules/checkpointR8x/cp_nat.py | 179 +++++++++++------- .../fw_modules/checkpointR8x/cp_rule.py | 4 +- .../fw_modules/checkpointR8x/fwcommon.py | 3 +- .../model_controllers/check_consistency.py | 2 +- 4 files changed, 113 insertions(+), 75 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index ea11d4c640..f9026e289d 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -1,84 +1,121 @@ -import json from typing import Any -from fw_modules.checkpointR8x.cp_rule import check_and_add_section_header, parse_single_rule +from fw_modules.checkpointR8x.cp_rule import parse_single_rule from fwo_log import FWOLogger from models.import_state import ImportState from models.rulebase import Rulebase -def normalize_nat_rules(native_config: dict[str, Any], import_state: ImportState, normalized_config: dict[str, Any]): +def normalize_nat_rules( + native_config: dict[str, Any], + import_state: ImportState, + normalized_config: dict[str, Any], +): native_nat_rulebases = native_config.get("nat_rulebases", []) if not native_nat_rulebases: return - for nat_rulebase in native_nat_rulebases: - if "nat_rule_chunks" in nat_rulebase: - # parse chunks - pass - else: - # parse rulebase - pass + for gateway in native_config["gateways"]: + for nat_rulebase in native_nat_rulebases: + if "nat_rule_chunks" not in nat_rulebase: + continue -def parse_nat_rulebase( - src_rulebase: dict[str, Any], - target_rulebase: Rulebase, - layer_name: str, - import_id: str, - section_header_uids: set[str], - parent_uid: str, - gateway: dict[str, Any], - policy_structure: list[dict[str, Any]], - debug_level: int = 0, - recursion_level: int = 1, -): - if recursion_level > 1000000: - raise Exception("ImportRecursionLimitReached(parse_nat_rulebase_json) from None") + normalized_nat_rulebase = Rulebase( + uid="nat-rulebase", + mgm_uid=import_state.mgm_details.uid, + name="NAT", + rules={}, + ) - if "nat_rule_chunks" in src_rulebase: - for chunk in src_rulebase["nat_rule_chunks"]: - if "rulebase" in chunk: - for rules_chunk in chunk["rulebase"]: - parse_nat_rulebase( - rules_chunk, - target_rulebase, - layer_name, - import_id, - section_header_uids, - parent_uid, - gateway, - policy_structure, - debug_level=debug_level, - recursion_level=recursion_level + 1, - ) - else: - FWOLogger.warning(f"parse_rule: found no rulebase in chunk:\n{json.dumps(chunk, indent=2)}") - else: - if "rulebase" in src_rulebase: - check_and_add_section_header(src_rulebase, target_rulebase, layer_name, import_id, section_header_uids) + normalized_gateway = next((gw for gw in normalized_config["gateways"] if gw["Uid"] == gateway["uid"]), None) - for rule in src_rulebase["rulebase"]: - (rule_match, rule_xlate) = parse_nat_rule_transform(rule) - parse_single_rule(rule_match, target_rulebase, layer_name, parent_uid, gateway, policy_structure) - parse_single_rule( # do not increase rule_num here - rule_xlate, target_rulebase, layer_name, parent_uid, gateway, policy_structure + if normalized_gateway is None: + FWOLogger.warning( + "Could not find normalized gateway for NAT rulebase, skipping: " + str(gateway["uid"]) ) + continue - if "rule-number" in src_rulebase: # rulebase is just a single rule (xlate rules do not count) - (rule_match, rule_xlate) = parse_nat_rule_transform(src_rulebase) - parse_single_rule(rule_match, target_rulebase, layer_name, parent_uid, gateway, policy_structure) - parse_single_rule( # do not increase rule_num here (xlate rules do not count) - rule_xlate, target_rulebase, layer_name, parent_uid, gateway, policy_structure + normalized_gateway["RulebaseLinks"].append( + { + "from_rulebase_uid": normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], + "to_rulebase_uid": normalized_nat_rulebase.uid, + "link_type": "ordered", + "is_initial": False, + "is_global": False, + "is_section": False, + } ) + for chunk in nat_rulebase["nat_rule_chunks"]: + if "rulebase" not in chunk: + continue + for src_rulebase in chunk["rulebase"]: + if "rulebase" in src_rulebase: + section_rulebase = Rulebase( + uid=src_rulebase["uid"], + mgm_uid=import_state.mgm_details.uid, + name=src_rulebase["name"], + rules={}, + ) + normalized_config["policies"].append(section_rulebase) + normalized_gateway["RulebaseLinks"].append( + { + "from_rulebase_uid": normalized_nat_rulebase.uid, + "to_rulebase_uid": section_rulebase.uid, + "link_type": "concatenated", + "is_initial": False, + "is_global": False, + "is_section": False, + } + ) + + for rule in src_rulebase["rulebase"]: + (rule_match, rule_xlate) = parse_nat_rule_transform(rule) + parse_single_rule( + rule_match, + section_rulebase, + section_rulebase.name, + section_rulebase.uid, + gateway, + native_config["policies"], + ) + parse_single_rule( # do not increase rule_num here + rule_xlate, + section_rulebase, + section_rulebase.name, + section_rulebase.uid, + gateway, + native_config["policies"], + ) + + if "rule-number" in src_rulebase: # rulebase is just a single rule (xlate rules do not count) + (rule_match, rule_xlate) = parse_nat_rule_transform(src_rulebase) + parse_single_rule( + rule_match, + normalized_nat_rulebase, + normalized_nat_rulebase.name, + normalized_nat_rulebase.uid, + gateway, + native_config["policies"], + ) + parse_single_rule( # do not increase rule_num here (xlate rules do not count) + rule_xlate, + normalized_nat_rulebase, + normalized_nat_rulebase.name, + normalized_nat_rulebase.uid, + gateway, + native_config["policies"], + ) + normalized_config["policies"].append(normalized_nat_rulebase) + -def parse_nat_rule_transform(xlate_rule_in: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: +def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: # TODO: cleanup certain fields (install-on, ....) - rule_match = { - "uid": xlate_rule_in["uid"], - "source": [xlate_rule_in["original-source"]], - "destination": [xlate_rule_in["original-destination"]], - "service": [xlate_rule_in["original-service"]], + nat_in_rule = { + "uid": nat_rule["uid"], + "source": [nat_rule["original-source"]], + "destination": [nat_rule["original-destination"]], + "service": [nat_rule["original-service"]], "action": {"name": "Drop"}, "track": {"type": {"name": "None"}}, "type": "nat", @@ -87,16 +124,16 @@ def parse_nat_rule_transform(xlate_rule_in: dict[str, Any]) -> tuple[dict[str, A "destination-negate": False, "service-negate": False, "install-on": [{"name": "Policy Targets"}], - "time": [{"name": "Any"}], - "enabled": xlate_rule_in["enabled"], - "comments": xlate_rule_in["comments"], + "time": "", + "enabled": nat_rule["enabled"], + "comments": nat_rule["comments"], "rule_type": "access", } - rule_xlate = { - "uid": xlate_rule_in["uid"], - "source": [xlate_rule_in["translated-source"]], - "destination": [xlate_rule_in["translated-destination"]], - "service": [xlate_rule_in["translated-service"]], + nat_out_rule = { + "uid": nat_rule["uid"], + "source": [nat_rule["translated-source"]], + "destination": [nat_rule["translated-destination"]], + "service": [nat_rule["translated-service"]], "action": {"name": "Drop"}, "track": {"type": {"name": "None"}}, "type": "nat", @@ -106,7 +143,7 @@ def parse_nat_rule_transform(xlate_rule_in: dict[str, Any]) -> tuple[dict[str, A "destination-negate": False, "service-negate": False, "install-on": [{"name": "Policy Targets"}], - "time": [{"name": "Any"}], + "time": "", "rule_type": "nat", } - return (rule_match, rule_xlate) + return (nat_in_rule, nat_out_rule) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py index 2ff24d6ba4..6bd44638c3 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py @@ -472,7 +472,7 @@ def check_and_add_section_header( src_rulebase: dict[str, Any], target_rulebase: Rulebase, layer_name: str, - import_id: str, + import_id: int, section_header_uids: set[str], ): # TODO: re-implement @@ -483,7 +483,7 @@ def insert_section_header_rule( _target_rulebase: Rulebase, _section_name: str, _layer_name: str, - _import_id: str, + _import_id: int, _src_rulebase_uid: str, _section_header_uids: set[str], _parent_uid: str, diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index cd0d8c2f00..7e9d0c84c5 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -229,7 +229,7 @@ def normalize_single_manager_config( cp_network.normalize_time_objects(native_config, normalized_config_dict) FWOLogger.info("completed normalizing time objects") cp_gateway.normalize_gateways(native_config, import_state, normalized_config_dict) - cp_nat.normalize_nat_rules(native_config, import_state, normalized_config_dict) + cp_rule.normalize_rulebases( native_config, native_config_global, @@ -238,6 +238,7 @@ def normalize_single_manager_config( normalized_config_global, is_global_loop_iteration, ) + cp_nat.normalize_nat_rules(native_config, import_state, normalized_config_dict) if not parsing_config_only: # get config from cp fw mgr cp_getter.logout(import_state.mgm_details.build_fw_api_string(), sid) FWOLogger.info("completed normalizing rulebases") diff --git a/roles/importer/files/importer/model_controllers/check_consistency.py b/roles/importer/files/importer/model_controllers/check_consistency.py index 97d035fd67..b918e08f39 100644 --- a/roles/importer/files/importer/model_controllers/check_consistency.py +++ b/roles/importer/files/importer/model_controllers/check_consistency.py @@ -475,7 +475,7 @@ def _check_rule_consistency(self, rulebases: list[Rulebase]) -> tuple[int, list[ if rule.rule_uid is None: rules_missing_uid += 1 continue - if rule.rule_uid in seen_rule_uids: + if rule.rule_uid in seen_rule_uids and rule.rule_type != "nat": duplicate_rule_uids.append(rule.rule_uid) seen_rule_uids.add(rule.rule_uid) if rule.rule_src == "" or rule.rule_dst == "": From dd5a7d3475d5726763295c8111a253f8481f55c9 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Sun, 29 Mar 2026 15:26:00 +0200 Subject: [PATCH 12/63] fix: Database duplication errors --- .../fw_modules/checkpointR8x/cp_nat.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index f9026e289d..29bdbae62b 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -14,7 +14,7 @@ def normalize_nat_rules( native_nat_rulebases = native_config.get("nat_rulebases", []) if not native_nat_rulebases: return - + seen_uids: set[str] = set() for gateway in native_config["gateways"]: for nat_rulebase in native_nat_rulebases: if "nat_rule_chunks" not in nat_rulebase: @@ -70,6 +70,10 @@ def normalize_nat_rules( ) for rule in src_rulebase["rulebase"]: + uid = rule.get("uid") + if uid in seen_uids: + continue + seen_uids.add(uid) (rule_match, rule_xlate) = parse_nat_rule_transform(rule) parse_single_rule( rule_match, @@ -89,6 +93,10 @@ def normalize_nat_rules( ) if "rule-number" in src_rulebase: # rulebase is just a single rule (xlate rules do not count) + uid = src_rulebase["uid"] + if uid in seen_uids: + continue + seen_uids.add(uid) (rule_match, rule_xlate) = parse_nat_rule_transform(src_rulebase) parse_single_rule( rule_match, @@ -112,29 +120,29 @@ def normalize_nat_rules( def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: # TODO: cleanup certain fields (install-on, ....) nat_in_rule = { - "uid": nat_rule["uid"], + "uid": nat_rule["uid"] + "-original", "source": [nat_rule["original-source"]], "destination": [nat_rule["original-destination"]], "service": [nat_rule["original-service"]], - "action": {"name": "Drop"}, + "action": {"name": "Drop", "type": "nat"}, "track": {"type": {"name": "None"}}, "type": "nat", "rule-number": 0, "source-negate": False, "destination-negate": False, "service-negate": False, - "install-on": [{"name": "Policy Targets"}], + "install-on": nat_rule["install-on"], "time": "", "enabled": nat_rule["enabled"], "comments": nat_rule["comments"], "rule_type": "access", } nat_out_rule = { - "uid": nat_rule["uid"], + "uid": nat_rule["uid"] + "-translated", "source": [nat_rule["translated-source"]], "destination": [nat_rule["translated-destination"]], "service": [nat_rule["translated-service"]], - "action": {"name": "Drop"}, + "action": None, "track": {"type": {"name": "None"}}, "type": "nat", "rule-number": 0, @@ -142,7 +150,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "source-negate": False, "destination-negate": False, "service-negate": False, - "install-on": [{"name": "Policy Targets"}], + "install-on": nat_rule["install-on"], "time": "", "rule_type": "nat", } From 2f1e37b32e2925a467a665f17c9548913c99af72 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Sun, 29 Mar 2026 16:15:20 +0200 Subject: [PATCH 13/63] fix: malformed object warning --- .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 29bdbae62b..441f497d60 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -124,8 +124,8 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "source": [nat_rule["original-source"]], "destination": [nat_rule["original-destination"]], "service": [nat_rule["original-service"]], - "action": {"name": "Drop", "type": "nat"}, - "track": {"type": {"name": "None"}}, + "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "-original-action"}], + "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "-original"}], "type": "nat", "rule-number": 0, "source-negate": False, @@ -142,8 +142,8 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "source": [nat_rule["translated-source"]], "destination": [nat_rule["translated-destination"]], "service": [nat_rule["translated-service"]], - "action": None, - "track": {"type": {"name": "None"}}, + "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "-translated-action"}], + "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "-translated"}], "type": "nat", "rule-number": 0, "enabled": True, From 78bcf2f05dc9684364ef03f43161366fe1924e82 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 30 Mar 2026 16:45:08 +0200 Subject: [PATCH 14/63] feat: Add stm_link_type --- roles/database/files/sql/creation/fworch-fill-stm.sql | 1 + roles/database/files/upgrade/9.0.16.sql | 1 + .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 roles/database/files/upgrade/9.0.16.sql diff --git a/roles/database/files/sql/creation/fworch-fill-stm.sql b/roles/database/files/sql/creation/fworch-fill-stm.sql index cde3924f23..a9db01b9f8 100644 --- a/roles/database/files/sql/creation/fworch-fill-stm.sql +++ b/roles/database/files/sql/creation/fworch-fill-stm.sql @@ -560,6 +560,7 @@ insert into stm_link_type (id, name) VALUES (2, 'ordered'); insert into stm_link_type (id, name) VALUES (3, 'inline'); insert into stm_link_type (id, name) VALUES (4, 'concatenated'); insert into stm_link_type (id, name) VALUES (5, 'domain'); +insert into stm_link_type (id, name) VALUES (6, 'nat'); -- insert into compliance.assessability_issue_type (type_id, type_name) VALUES (1, 'empty group'); -- insert into compliance.assessability_issue_type (type_id, type_name) VALUES (2, 'broadcast address'); diff --git a/roles/database/files/upgrade/9.0.16.sql b/roles/database/files/upgrade/9.0.16.sql new file mode 100644 index 0000000000..b89901f26a --- /dev/null +++ b/roles/database/files/upgrade/9.0.16.sql @@ -0,0 +1 @@ +insert into stm_link_type (id, name) VALUES (6, 'nat') ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 441f497d60..87aeb93978 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -39,7 +39,7 @@ def normalize_nat_rules( { "from_rulebase_uid": normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], "to_rulebase_uid": normalized_nat_rulebase.uid, - "link_type": "ordered", + "link_type": "nat", "is_initial": False, "is_global": False, "is_section": False, @@ -62,7 +62,7 @@ def normalize_nat_rules( { "from_rulebase_uid": normalized_nat_rulebase.uid, "to_rulebase_uid": section_rulebase.uid, - "link_type": "concatenated", + "link_type": "nat", "is_initial": False, "is_global": False, "is_section": False, @@ -118,7 +118,6 @@ def normalize_nat_rules( def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: - # TODO: cleanup certain fields (install-on, ....) nat_in_rule = { "uid": nat_rule["uid"] + "-original", "source": [nat_rule["original-source"]], From b0fdcd14e08176a6ee645e3ae8214c7f07559400 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 30 Mar 2026 19:54:17 +0200 Subject: [PATCH 15/63] wip: Proper policy mapping & nat import --- .../fw_modules/checkpointR8x/cp_getter.py | 27 +++ .../fw_modules/checkpointR8x/cp_nat.py | 79 +++++---- .../fw_modules/checkpointR8x/fwcommon.py | 155 ++++++++++-------- 3 files changed, 159 insertions(+), 102 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py index 86197d6133..0bef8b3cba 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py @@ -976,3 +976,30 @@ def get_object_details_from_api(uid_missing_obj: str, sid: str = "", apiurl: str return obj FWOLogger.warning(f"missing nw obj of unexpected type '{obj_type}': {uid_missing_obj}") return {} + + +def get_gateways_and_servers(sid: str = "", apiurl: str = "") -> list[dict[str, Any]]: + """Fetch gateways and servers from the API.""" + current = 0 + total = current + 1 + + gateways_and_servers: list[dict[str, Any]] = [] + + while current < total: + try: + result = cp_api_call(apiurl, "show-gateways-and-servers", {"details-level": "full"}, sid) + except Exception as e: + raise FwoImporterError(f"error while trying to get gateways and servers: {e}") + + if result is None or "objects" not in result: + raise FwoImporterError("no objects received while trying to get gateways and servers") + + gateways_and_servers.extend(result["objects"]) + + if "total" not in result or "to" not in result: + raise FwoImporterError("result does not contain total or to field while trying to get gateways and servers") + + total = result["total"] + current = result["to"] + + return gateways_and_servers diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 87aeb93978..e4b240c6d9 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -35,16 +35,23 @@ def normalize_nat_rules( ) continue - normalized_gateway["RulebaseLinks"].append( - { - "from_rulebase_uid": normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], - "to_rulebase_uid": normalized_nat_rulebase.uid, - "link_type": "nat", - "is_initial": False, - "is_global": False, - "is_section": False, - } - ) + if not any( + link + for link in normalized_gateway["RulebaseLinks"] + if link["to_rulebase_uid"] == normalized_nat_rulebase.uid + and link["link_type"] == "nat" + and link["from_rulebase_uid"] == normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"] + ): + normalized_gateway["RulebaseLinks"].append( + { + "from_rulebase_uid": normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], + "to_rulebase_uid": normalized_nat_rulebase.uid, + "link_type": "ordered", + "is_initial": False, + "is_global": False, + "is_section": False, + } + ) for chunk in nat_rulebase["nat_rule_chunks"]: if "rulebase" not in chunk: @@ -57,23 +64,33 @@ def normalize_nat_rules( name=src_rulebase["name"], rules={}, ) - normalized_config["policies"].append(section_rulebase) - normalized_gateway["RulebaseLinks"].append( - { - "from_rulebase_uid": normalized_nat_rulebase.uid, - "to_rulebase_uid": section_rulebase.uid, - "link_type": "nat", - "is_initial": False, - "is_global": False, - "is_section": False, - } - ) + + if not any(rb for rb in normalized_config["policies"] if rb.uid == section_rulebase.uid): + normalized_config["policies"].append(section_rulebase) + + if not any( + link + for link in normalized_gateway["RulebaseLinks"] + if link["to_rulebase_uid"] == section_rulebase.uid + and link["link_type"] == "nat" + and link["from_rulebase_uid"] == normalized_nat_rulebase.uid + ): + normalized_gateway["RulebaseLinks"].append( + { + "from_rulebase_uid": normalized_nat_rulebase.uid, + "to_rulebase_uid": section_rulebase.uid, + "link_type": "concatenated", + "is_initial": False, + "is_global": False, + "is_section": False, + } + ) for rule in src_rulebase["rulebase"]: uid = rule.get("uid") if uid in seen_uids: continue - seen_uids.add(uid) + # seen_uids.add(uid) (rule_match, rule_xlate) = parse_nat_rule_transform(rule) parse_single_rule( rule_match, @@ -96,7 +113,7 @@ def normalize_nat_rules( uid = src_rulebase["uid"] if uid in seen_uids: continue - seen_uids.add(uid) + # seen_uids.add(uid) (rule_match, rule_xlate) = parse_nat_rule_transform(src_rulebase) parse_single_rule( rule_match, @@ -114,17 +131,19 @@ def normalize_nat_rules( gateway, native_config["policies"], ) - normalized_config["policies"].append(normalized_nat_rulebase) + + if not any(rb for rb in normalized_config["policies"] if rb.uid == normalized_nat_rulebase.uid): + normalized_config["policies"].append(normalized_nat_rulebase) def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: nat_in_rule = { - "uid": nat_rule["uid"] + "-original", + "uid": nat_rule["uid"] + "_original", "source": [nat_rule["original-source"]], "destination": [nat_rule["original-destination"]], "service": [nat_rule["original-service"]], - "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "-original-action"}], - "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "-original"}], + "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "_original-action"}], + "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "_original"}], "type": "nat", "rule-number": 0, "source-negate": False, @@ -137,12 +156,12 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "rule_type": "access", } nat_out_rule = { - "uid": nat_rule["uid"] + "-translated", + "uid": nat_rule["uid"] + "_translated", "source": [nat_rule["translated-source"]], "destination": [nat_rule["translated-destination"]], "service": [nat_rule["translated-service"]], - "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "-translated-action"}], - "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "-translated"}], + "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "_translated-action"}], + "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "_translated"}], "type": "nat", "rule-number": 0, "enabled": True, diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index 7e9d0c84c5..44cf3a7444 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -274,6 +274,7 @@ def get_rules(native_config: dict[str, Any], import_state: ImportState) -> int: manager_details, policy_structure=policy_structure, ) + gateways_and_servers = cp_getter.get_gateways_and_servers(sid, cp_manager_api_base_url) process_devices( manager_details, @@ -289,6 +290,7 @@ def get_rules(native_config: dict[str, Any], import_state: ImportState) -> int: ], # globalSid should not be None but is when the first manager is not supermanager native_config["domains"][0], import_state, + gateways_and_servers, ) native_config["domains"][manager_index].update({"policies": policy_structure}) @@ -350,6 +352,7 @@ def process_devices( native_config_domain: dict[str, Any], native_config_global_domain: dict[str, Any], import_state: ImportState, + gateways_and_servers: list[dict[str, Any]], ) -> None: for device in manager_details.devices: if device["importDisabled"] and not import_state.force_import: @@ -357,9 +360,38 @@ def process_devices( device_config: dict[str, Any] = initialize_device_config(device) if not device_config: continue + # found_gateway = next((gw for gw in gateways_and_servers if gw["uid"] == device["uid"]), None) + found_gateway = next((gw for gw in gateways_and_servers if gw["name"] == "CP_SMS_GW_Test4FWO"), None) + if found_gateway is None: + FWOLogger.warning("Could not find gateway for device, skipping: " + str(device["uid"])) + native_config_domain["gateways"].append(device_config) + continue + + if "policy" not in found_gateway: + FWOLogger.warning("Could not find policy for gateway, skipping: " + str(device["uid"])) + native_config_domain["gateways"].append(device_config) + continue + + gateway_policy = found_gateway["policy"] + if "access-policy-name" not in gateway_policy: + FWOLogger.warning("Could not find access policy for gateway, skipping: " + str(device["uid"])) + native_config_domain["gateways"].append(device_config) + continue + + policy = next( + (policy for policy in policy_structure if policy["name"] == gateway_policy["access-policy-name"]), None + ) + + if not policy: + FWOLogger.warning( + "Could not find policy structure for gateway policy, skipping: " + + str(gateway_policy["access-policy-name"]) + ) + native_config_domain["gateways"].append(device_config) + continue ordered_layer_uids: list[str] = get_ordered_layer_uids( - policy_structure, device_config, manager_details.get_domain_string() + policy, device_config, manager_details.get_domain_string() ) if not ordered_layer_uids: FWOLogger.warning(f"No ordered layers found for device: {device_config['name']}") @@ -383,16 +415,11 @@ def process_devices( else: define_initial_rulebase_links( device_config, - policy_structure, + policy, is_global=False, native_config_domain=native_config_global_domain, ) - policy_structure_dict = next( - (policy for policy in policy_structure if policy["uid"] == ordered_layer_uids[0]), - {"uid": ordered_layer_uids[0]}, - ) - add_ordered_layers_to_native_config( ordered_layer_uids, get_rules_params(import_state), @@ -402,10 +429,10 @@ def process_devices( device_config, is_global=False, global_ordered_layer_count=global_ordered_layer_count, - policy_structure=policy_structure_dict, + policy_structure=policy, ) - handle_nat_rules(native_config_domain, sid, import_state, policy_structure) + handle_nat_rules(native_config_domain, sid, import_state, policy) native_config_domain["gateways"].append(device_config) @@ -443,7 +470,7 @@ def handle_global_rulebase_links( continue for global_policy in global_policy_structure: if global_policy["name"] == global_assignment["global-access-policy"]: - global_ordered_layer_uids = get_ordered_layer_uids([global_policy], device_config, global_domain) + global_ordered_layer_uids = get_ordered_layer_uids(global_policy, device_config, global_domain) if not global_ordered_layer_uids: FWOLogger.warning(f"No access layer for global policy: {global_policy['name']}") break @@ -465,7 +492,7 @@ def handle_global_rulebase_links( ordered_layer_uids, native_config_global_domain, global_policy_rulebases_uid_list, - global_policy_structure, + global_policy, ) return global_ordered_layer_count @@ -478,14 +505,14 @@ def define_global_rulebase_link( ordered_layer_uids: list[str], native_config_global_domain: dict[str, Any], global_policy_rulebases_uid_list: list[str], - policy_structure: list[dict[str, Any]], + policy: dict[str, Any], ): """ Links initial and placeholder rule for global rulebases """ define_initial_rulebase_links( device_config, - policy_structure, + policy, is_global=True, native_config_domain=native_config_global_domain, ) @@ -521,27 +548,27 @@ def define_global_rulebase_link( def define_initial_rulebase_links( device_config: dict[str, Any], - policy_structures: list[dict[str, Any]], + policy: dict[str, Any], is_global: bool, native_config_domain: dict[str, Any] | None, ): if native_config_domain is None: native_config_domain = {"rulebases": []} - for policy in policy_structures: - if not any(rb["uid"] == policy["uid"] for rb in native_config_domain["rulebases"]): - native_config_domain["rulebases"].append({"uid": policy["uid"], "name": policy["name"], "chunks": []}) - device_config["rulebase_links"].append( - { - "from_rulebase_uid": None, - "from_rule_uid": None, - "to_rulebase_uid": policy["uid"], - "type": "ordered", - "is_global": is_global, - "is_initial": True, - "is_section": False, - } - ) + if not any(rb["uid"] == policy["uid"] for rb in native_config_domain["rulebases"]): + native_config_domain["rulebases"].append({"uid": policy["uid"], "name": policy["name"], "chunks": []}) + + device_config["rulebase_links"].append( + { + "from_rulebase_uid": None, + "from_rule_uid": None, + "to_rulebase_uid": policy["uid"], + "type": "ordered", + "is_global": is_global, + "is_initial": True, + "is_section": False, + } + ) def get_rules_params(import_state: ImportState) -> dict[str, Any]: @@ -553,26 +580,23 @@ def get_rules_params(import_state: ImportState) -> dict[str, Any]: } -def handle_nat_rules( - native_config_domain: dict[str, Any], sid: str, import_state: ImportState, policy_structure: list[dict[str, Any]] -): - for policy in policy_structure: - show_params_rules: dict[str, Any] = { - "limit": import_state.fwo_config.api_fetch_size, - "use-object-dictionary": cp_const.use_object_dictionary, - "package": policy["name"], - } - FWOLogger.debug(f"Getting NAT rules for package: {policy['name']}", 4) - nat_rules = cp_getter.get_nat_rules_from_api_as_dict( - import_state.mgm_details.build_fw_api_string(), - sid, - show_params_rules, - native_config_domain=native_config_domain, - ) - if nat_rules: - native_config_domain["nat_rulebases"].append(nat_rules) - else: - native_config_domain["nat_rulebases"].append({"nat_rule_chunks": []}) +def handle_nat_rules(native_config_domain: dict[str, Any], sid: str, import_state: ImportState, policy: dict[str, Any]): + show_params_rules: dict[str, Any] = { + "limit": import_state.fwo_config.api_fetch_size, + "use-object-dictionary": cp_const.use_object_dictionary, + "package": policy["name"], + } + FWOLogger.debug(f"Getting NAT rules for package: {policy['name']}", 4) + nat_rules = cp_getter.get_nat_rules_from_api_as_dict( + import_state.mgm_details.build_fw_api_string(), + sid, + show_params_rules, + native_config_domain=native_config_domain, + ) + if nat_rules: + native_config_domain["nat_rulebases"].append(nat_rules) + else: + native_config_domain["nat_rulebases"].append({"nat_rule_chunks": []}) def add_ordered_layers_to_native_config( @@ -631,36 +655,23 @@ def add_ordered_layers_to_native_config( return policy_rulebases_uid_list -def get_ordered_layer_uids( - policy_structure: list[dict[str, Any]], device_config: dict[str, Any], domain: str | None -) -> list[str]: +def get_ordered_layer_uids(policy: dict[str, Any], device_config: dict[str, Any], domain: str | None) -> list[str]: """ Get UIDs of ordered layers for policy of device """ - ordered_layer_uids: list[str] = [] - for policy in policy_structure: - found_target_in_policy = False - if "uid" in policy: - ordered_layer_uids.extend([policy["uid"]]) - for target in policy["targets"]: - if target["uid"] == device_config["uid"] or target["uid"] == "all": - found_target_in_policy = True - if found_target_in_policy: - append_access_layer_uid(policy, domain, ordered_layer_uids) - + ordered_layer_uids: list[str] = [policy["uid"]] + for target in policy["targets"]: + if target["uid"] == device_config["uid"] or target["uid"] == "all": + ordered_layer_uids.extend( + [ + access_layer["uid"] + for access_layer in policy["access-layers"] + if access_layer["domain"] == domain or domain == "" + ] + ) return ordered_layer_uids -def append_access_layer_uid(policy: dict[str, Any], domain: str | None, ordered_layer_uids: list[str]) -> None: - ordered_layer_uids.extend( - [ - access_layer["uid"] - for access_layer in policy["access-layers"] - if access_layer["domain"] == domain or domain == "" - ] - ) - - def get_objects(native_config_dict: dict[str, Any], import_state: ImportState) -> int: show_params_objs = {"limit": import_state.fwo_config.api_fetch_size} manager_details_list = create_ordered_manager_list(import_state) From 3e359bddf8f8e1ba151eb06603cd8e07c07a0f2b Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 1 Apr 2026 12:19:58 +0200 Subject: [PATCH 16/63] fix: multiple NAT objects --- .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index e4b240c6d9..45a1c208b9 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -21,9 +21,9 @@ def normalize_nat_rules( continue normalized_nat_rulebase = Rulebase( - uid="nat-rulebase", + uid="nat-rulebase-" + gateway["uid"], mgm_uid=import_state.mgm_details.uid, - name="NAT", + name="NAT Rulebase for " + gateway["name"], rules={}, ) @@ -39,7 +39,7 @@ def normalize_nat_rules( link for link in normalized_gateway["RulebaseLinks"] if link["to_rulebase_uid"] == normalized_nat_rulebase.uid - and link["link_type"] == "nat" + # and link["link_type"] == "nat" and link["from_rulebase_uid"] == normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"] ): normalized_gateway["RulebaseLinks"].append( @@ -72,7 +72,7 @@ def normalize_nat_rules( link for link in normalized_gateway["RulebaseLinks"] if link["to_rulebase_uid"] == section_rulebase.uid - and link["link_type"] == "nat" + # and link["link_type"] == "nat" and link["from_rulebase_uid"] == normalized_nat_rulebase.uid ): normalized_gateway["RulebaseLinks"].append( From b1c1d0d557188a3d2fed3f7cccb677c196bca2db Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 1 Apr 2026 13:49:42 +0200 Subject: [PATCH 17/63] wip: NAT rules working as rules --- .../importer/files/importer/fw_modules/checkpointR8x/cp_nat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 45a1c208b9..4354cae10f 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -23,7 +23,7 @@ def normalize_nat_rules( normalized_nat_rulebase = Rulebase( uid="nat-rulebase-" + gateway["uid"], mgm_uid=import_state.mgm_details.uid, - name="NAT Rulebase for " + gateway["name"], + name="NAT", rules={}, ) From 37ba8d26cc12bb5c00f9a9e15e7bf3f5cc67cd8c Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Wed, 8 Apr 2026 18:02:05 +0200 Subject: [PATCH 18/63] chore: Revert sql upgrade script change --- roles/database/files/upgrade/9.0.16.sql | 3 +-- roles/database/files/upgrade/9.0.xx.sql | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 roles/database/files/upgrade/9.0.xx.sql diff --git a/roles/database/files/upgrade/9.0.16.sql b/roles/database/files/upgrade/9.0.16.sql index 8c56beefaf..889860f863 100644 --- a/roles/database/files/upgrade/9.0.16.sql +++ b/roles/database/files/upgrade/9.0.16.sql @@ -13,5 +13,4 @@ ALTER TABLE rule_owner DROP CONSTRAINT IF EXISTS rule_owner_matched_objects_for_ip_based; ALTER TABLE rule_owner ADD CONSTRAINT rule_owner_matched_objects_for_ip_based -CHECK ( owner_mapping_source_id != 1 OR matched_objects IS NOT NULL ); -insert into stm_link_type (id, name) VALUES (6, 'nat') ON CONFLICT DO NOTHING; \ No newline at end of file +CHECK ( owner_mapping_source_id != 1 OR matched_objects IS NOT NULL ); \ No newline at end of file diff --git a/roles/database/files/upgrade/9.0.xx.sql b/roles/database/files/upgrade/9.0.xx.sql new file mode 100644 index 0000000000..b89901f26a --- /dev/null +++ b/roles/database/files/upgrade/9.0.xx.sql @@ -0,0 +1 @@ +insert into stm_link_type (id, name) VALUES (6, 'nat') ON CONFLICT DO NOTHING; \ No newline at end of file From a47b191f3a3af2dc6e96130bce25b7d5a2fc8bc5 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Wed, 8 Apr 2026 18:59:56 +0200 Subject: [PATCH 19/63] fix: NAT query --- roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs index ac0bbebd77..0fc5f630e3 100644 --- a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs +++ b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs @@ -394,6 +394,7 @@ query natRulesReport ({paramString}) name: dev_name rulebase_links(where: {{ {query.RulebaseLinkWhereStatement} }}) {{ + rulebase {{ {query.OpenRulesTable} {limitOffsetString} where: {{ nat_rule: {{_eq: true}}, ruleByXlateRule: {{}} {query.RuleWhereStatement} }} @@ -405,6 +406,7 @@ query natRulesReport ({paramString}) }} }} }} + }} }}"; } From 5ac8a55a5c260878af0227c242cf30940bc6a31d Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Thu, 9 Apr 2026 17:39:08 +0200 Subject: [PATCH 20/63] wip: NAT frontend --- .../fw_modules/checkpointR8x/cp_nat.py | 16 +++++++- .../fw_modules/checkpointR8x/fwcommon.py | 4 +- .../FWO.Report.Filter/DynGraphqlQuery.cs | 41 +++++++++++++------ roles/lib/files/FWO.Report/ReportBase.cs | 2 +- roles/lib/files/FWO.Report/ReportNatRules.cs | 3 +- 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 4354cae10f..3e51803b94 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -35,6 +35,18 @@ def normalize_nat_rules( ) continue + if len(normalized_gateway["RulebaseLinks"]) == 0: + normalized_gateway["RulebaseLinks"].append( + { + "from_rulebase_uid": normalized_gateway["Uid"], + "to_rulebase_uid": normalized_nat_rulebase.uid, + "link_type": "nat", + "is_initial": True, + "is_global": False, + "is_section": False, + } + ) + if not any( link for link in normalized_gateway["RulebaseLinks"] @@ -46,7 +58,7 @@ def normalize_nat_rules( { "from_rulebase_uid": normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], "to_rulebase_uid": normalized_nat_rulebase.uid, - "link_type": "ordered", + "link_type": "nat", "is_initial": False, "is_global": False, "is_section": False, @@ -79,7 +91,7 @@ def normalize_nat_rules( { "from_rulebase_uid": normalized_nat_rulebase.uid, "to_rulebase_uid": section_rulebase.uid, - "link_type": "concatenated", + "link_type": "nat", "is_initial": False, "is_global": False, "is_section": False, diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index 44cf3a7444..1af07a5839 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -360,8 +360,8 @@ def process_devices( device_config: dict[str, Any] = initialize_device_config(device) if not device_config: continue - # found_gateway = next((gw for gw in gateways_and_servers if gw["uid"] == device["uid"]), None) - found_gateway = next((gw for gw in gateways_and_servers if gw["name"] == "CP_SMS_GW_Test4FWO"), None) + found_gateway = next((gw for gw in gateways_and_servers if gw["uid"] == device["uid"]), None) + #found_gateway = next((gw for gw in gateways_and_servers if gw["name"] == "CP_SMS_Phys_GWSingle_Test4FWO"), None) if found_gateway is None: FWOLogger.warning("Could not find gateway for device, skipping: " + str(device["uid"])) native_config_domain["gateways"].append(device_config) diff --git a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs index 0fc5f630e3..9479884641 100644 --- a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs +++ b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs @@ -392,21 +392,38 @@ query natRulesReport ({paramString}) {{ id: dev_id name: dev_name - rulebase_links(where: {{ {query.RulebaseLinkWhereStatement} }}) + rulebase_links(where: {{ {query.RulebaseLinkWhereStatement}, stm_link_type: {{id: {{_eq: 6}} }} }}) {{ - rulebase {{ - {query.OpenRulesTable} - {limitOffsetString} - where: {{ nat_rule: {{_eq: true}}, ruleByXlateRule: {{}} {query.RuleWhereStatement} }} - order_by: {{ rule_num_numeric: asc }} ) - {{ - mgm_id: mgm_id - ...{(filter.Detailed ? "natRuleDetails" : "natRuleOverview")} - }} + linkType: stm_link_type {{ + name + id + }} + link_type + is_initial + is_global + is_section + gw_id + from_rule_id + from_rulebase_id + to_rulebase_id + created + removed }} }} - }} - }} + rulebases {{ + name + uid + id + {query.OpenRulesTable} + {limitOffsetString} + where: {{ {query.RuleWhereStatement} }} + order_by: {{ rule_num_numeric: asc }} ) + {{ + mgm_id: mgm_id + ...{(filter.Detailed ? "natRuleDetails" : "natRuleOverview")} + }} + }} + }} }}"; } diff --git a/roles/lib/files/FWO.Report/ReportBase.cs b/roles/lib/files/FWO.Report/ReportBase.cs index 7fc53e43b4..be3c67698a 100644 --- a/roles/lib/files/FWO.Report/ReportBase.cs +++ b/roles/lib/files/FWO.Report/ReportBase.cs @@ -162,7 +162,7 @@ public static ReportBase ConstructReport(ReportTemplate reportFilter, UserConfig ReportType.Changes => new ReportChanges(query, userConfig, repType, reportFilter.ReportParams.TimeFilter, reportFilter.IncludeObjectsInReportChanges, reportFilter.IncludeObjectsInReportChangesUiPresesed), ReportType.ResolvedChanges => new ReportChanges(query, userConfig, repType, reportFilter.ReportParams.TimeFilter, reportFilter.IncludeObjectsInReportChanges, reportFilter.IncludeObjectsInReportChangesUiPresesed), ReportType.ResolvedChangesTech => new ReportChanges(query, userConfig, repType, reportFilter.ReportParams.TimeFilter, reportFilter.IncludeObjectsInReportChanges, reportFilter.IncludeObjectsInReportChangesUiPresesed), - ReportType.NatRules => new ReportNatRules(query, userConfig, repType), + ReportType.NatRules => new ReportNatRules(query, userConfig, repType, ruleTreeBuilder), ReportType.Recertification => new ReportRules(query, userConfig, repType, ruleTreeBuilder), ReportType.UnusedRules => new ReportRules(query, userConfig, repType, ruleTreeBuilder), ReportType.Connections => new ReportConnections(query, userConfig, repType), diff --git a/roles/lib/files/FWO.Report/ReportNatRules.cs b/roles/lib/files/FWO.Report/ReportNatRules.cs index 123c3b361b..11cac4395c 100644 --- a/roles/lib/files/FWO.Report/ReportNatRules.cs +++ b/roles/lib/files/FWO.Report/ReportNatRules.cs @@ -2,6 +2,7 @@ using FWO.Config.Api; using FWO.Data; using FWO.Report.Filter; +using FWO.Services.RuleTreeBuilder; using FWO.Ui.Display; using System.Text; @@ -9,7 +10,7 @@ namespace FWO.Report { public class ReportNatRules : ReportRules { - public ReportNatRules(DynGraphqlQuery query, UserConfig userConfig, ReportType reportType) : base(query, userConfig, reportType) { } + public ReportNatRules(DynGraphqlQuery query, UserConfig userConfig, ReportType reportType, IRuleTreeBuilder? ruleTreeBuilder = null) : base(query, userConfig, reportType, ruleTreeBuilder) { } private const int ColumnCount = 12; From bce860f6d5a6de112bbe32554f9c3a4685dfc16a Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Sun, 12 Apr 2026 12:59:31 +0200 Subject: [PATCH 21/63] wip: NAT report --- .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 3 +++ .../files/importer/fw_modules/checkpointR8x/cp_rule.py | 5 +++++ .../files/importer/model_controllers/fwconfig_import_rule.py | 5 ++++- roles/importer/files/importer/models/rule.py | 3 +++ roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs | 2 ++ 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 3e51803b94..fb1ab52791 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -134,6 +134,8 @@ def normalize_nat_rules( normalized_nat_rulebase.uid, gateway, native_config["policies"], + xlate_rule_id=rule_xlate["uid"], # TODO: needs numeric id after inserting into DB, update later + nat_rule=True, ) parse_single_rule( # do not increase rule_num here (xlate rules do not count) rule_xlate, @@ -142,6 +144,7 @@ def normalize_nat_rules( normalized_nat_rulebase.uid, gateway, native_config["policies"], + nat_rule=True, ) if not any(rb for rb in normalized_config["policies"] if rb.uid == normalized_nat_rulebase.uid): diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py index 6bd44638c3..c42bea7b4f 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py @@ -306,6 +306,8 @@ def parse_single_rule( parent_uid: str | None, gateway: dict[str, Any], policy_structure: list[dict[str, Any]], + xlate_rule_id: int | None = None, + nat_rule: bool = False, ): # reference to domain rule layer, filling up basic fields if not ( @@ -385,9 +387,12 @@ def parse_single_rule( "last_change_admin": sanitize(last_change_admin), "parent_rule_uid": sanitize(parent_rule_uid), "last_hit": sanitize(last_hit), + "nat_rule": nat_rule, } if comments is not None: rule["rule_comment"] = sanitize(comments) + if xlate_rule_id is not None: + rule["xlate_rule"] = xlate_rule_id rulebase.rules.update({rule["rule_uid"]: RuleNormalized(**rule)}) diff --git a/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py b/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py index 153efc9ca5..dc52b9b68a 100644 --- a/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py +++ b/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py @@ -119,6 +119,9 @@ def update_rulebase_diffs(self, prev_config: FwConfigNormalized) -> None: } ) + # TODO: NAT rules: The rules need to be inserted into the database first to get the rule_id's for the NAT rules. + # then, we have to update the xlate_rule field because we need the database (numeric) id's there. + self.uid2id_mapper.add_rule_mappings(inserted_rule_ids) refs_added = self.add_new_refs(prev_config) @@ -1134,7 +1137,7 @@ def prepare_rule_for_import(self, rule: RuleNormalized, rulebase_uid: str) -> Ru rule_from_zone=None, # TODO: to be removed or changed to string of joined zone names rule_to_zone=None, # TODO: to be removed or changed to string of joined zone names access_rule=True, - nat_rule=False, + nat_rule=rule.nat_rule, is_global=False, rulebase_id=rulebase_id, rule_create=self.import_details.state.import_id, diff --git a/roles/importer/files/importer/models/rule.py b/roles/importer/files/importer/models/rule.py index a018c1a77e..53164bed13 100644 --- a/roles/importer/files/importer/models/rule.py +++ b/roles/importer/files/importer/models/rule.py @@ -64,6 +64,9 @@ class RuleNormalized(BaseModel): # noqa: PLW1641 rule_src_zone: str | None = None rule_dst_zone: str | None = None rule_head_text: str | None = None + xlate_rule: int | None = None + nat_rule: bool = False + nat_rule: bool = False def __eq__(self, other: object) -> bool: if not isinstance(other, RuleNormalized): diff --git a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs index 9479884641..ea52da3c58 100644 --- a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs +++ b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs @@ -387,11 +387,13 @@ query natRulesReport ({paramString}) management({mgmtWhereString}) {{ id: mgm_id + uid: mgm_uid name: mgm_name devices ({devWhereStringDefault}) {{ id: dev_id name: dev_name + uid: dev_uid rulebase_links(where: {{ {query.RulebaseLinkWhereStatement}, stm_link_type: {{id: {{_eq: 6}} }} }}) {{ linkType: stm_link_type {{ From 6d373aba194869008b3655c32b5d1ee7c7674734 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 13 Apr 2026 12:11:36 +0200 Subject: [PATCH 22/63] wip: NAT import --- .../fw_modules/checkpointR8x/cp_nat.py | 7 ++++--- .../fw_modules/checkpointR8x/cp_rule.py | 8 +++----- .../model_controllers/fwconfig_import_rule.py | 18 ++++++++++++++---- roles/importer/files/importer/models/rule.py | 3 +-- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index fb1ab52791..1a864e7e25 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -134,8 +134,6 @@ def normalize_nat_rules( normalized_nat_rulebase.uid, gateway, native_config["policies"], - xlate_rule_id=rule_xlate["uid"], # TODO: needs numeric id after inserting into DB, update later - nat_rule=True, ) parse_single_rule( # do not increase rule_num here (xlate rules do not count) rule_xlate, @@ -144,7 +142,6 @@ def normalize_nat_rules( normalized_nat_rulebase.uid, gateway, native_config["policies"], - nat_rule=True, ) if not any(rb for rb in normalized_config["policies"] if rb.uid == normalized_nat_rulebase.uid): @@ -169,6 +166,8 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "enabled": nat_rule["enabled"], "comments": nat_rule["comments"], "rule_type": "access", + "nat_rule": True, + "xlate_rule_uid": nat_rule["uid"] + "_translated", } nat_out_rule = { "uid": nat_rule["uid"] + "_translated", @@ -186,5 +185,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "install-on": nat_rule["install-on"], "time": "", "rule_type": "nat", + "nat_rule": True, + "xlate_rule_uid": nat_rule["uid"] + "_translated", } return (nat_in_rule, nat_out_rule) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py index c42bea7b4f..394f474c4f 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py @@ -306,8 +306,6 @@ def parse_single_rule( parent_uid: str | None, gateway: dict[str, Any], policy_structure: list[dict[str, Any]], - xlate_rule_id: int | None = None, - nat_rule: bool = False, ): # reference to domain rule layer, filling up basic fields if not ( @@ -387,12 +385,12 @@ def parse_single_rule( "last_change_admin": sanitize(last_change_admin), "parent_rule_uid": sanitize(parent_rule_uid), "last_hit": sanitize(last_hit), - "nat_rule": nat_rule, + "nat_rule": bool(native_rule.get("nat_rule", False)), } if comments is not None: rule["rule_comment"] = sanitize(comments) - if xlate_rule_id is not None: - rule["xlate_rule"] = xlate_rule_id + if native_rule.get("xlate_rule_uid") is not None: + rule["xlate_rule_uid"] = sanitize(native_rule["xlate_rule_uid"]) rulebase.rules.update({rule["rule_uid"]: RuleNormalized(**rule)}) diff --git a/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py b/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py index dc52b9b68a..a0855c29c2 100644 --- a/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py +++ b/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py @@ -115,14 +115,22 @@ def update_rulebase_diffs(self, prev_config: FwConfigNormalized) -> None: num_inserted_rules, inserted_rule_ids = self.add_new_rules( { rule_uid: (curr_rules[rule_uid], curr_rule_to_rulebase[rule_uid]) - for rule_uid in (added_rule_uids | changed_rule_uids) + for rule_uid in (added_rule_uids | changed_rule_uids) if curr_rules[rule_uid].xlate_rule_uid is None } ) + self.uid2id_mapper.add_rule_mappings(inserted_rule_ids) - # TODO: NAT rules: The rules need to be inserted into the database first to get the rule_id's for the NAT rules. - # then, we have to update the xlate_rule field because we need the database (numeric) id's there. + # add new NAT rules separately after all non-NAT rules have been added, to ensure that all xlate rules are already in the database + # and can be referenced by their new numeric id in the xlate_rule field of the NAT rules + num_inserted_nat_rules, inserted_nat_rule_ids = self.add_new_rules( + { + rule_uid: (curr_rules[rule_uid], curr_rule_to_rulebase[rule_uid]) + for rule_uid in (added_rule_uids | changed_rule_uids) if curr_rules[rule_uid].xlate_rule_uid is not None + } + ) + num_inserted_rules += num_inserted_nat_rules + self.uid2id_mapper.add_rule_mappings(inserted_nat_rule_ids) - self.uid2id_mapper.add_rule_mappings(inserted_rule_ids) refs_added = self.add_new_refs(prev_config) num_set_removed_rules, _removed_rule_ids = self.mark_rules_removed(list(removed_rule_uids | changed_rule_uids)) @@ -1113,6 +1121,7 @@ def update_rule_enforced_on_gateway(self, changed_rule_uids: set[str]) -> tuple[ def prepare_rule_for_import(self, rule: RuleNormalized, rulebase_uid: str) -> Rule: rulebase_id = self.uid2id_mapper.get_rulebase_id(rulebase_uid) + xlate_rule_id = self.uid2id_mapper.get_rule_id(rule.xlate_rule_uid) if rule.xlate_rule_uid else None return Rule( mgm_id=self.import_details.state.mgm_details.current_mgm_id, rule_num=rule.rule_num, @@ -1148,6 +1157,7 @@ def prepare_rule_for_import(self, rule: RuleNormalized, rulebase_uid: str) -> Ru rule_head_text=rule.rule_head_text, rule_installon=rule.rule_installon, last_change_admin=None, # TODO: get id from rule.last_change_admin + xlate_rule=xlate_rule_id, ) def write_changelog_rules( diff --git a/roles/importer/files/importer/models/rule.py b/roles/importer/files/importer/models/rule.py index 53164bed13..961446d5aa 100644 --- a/roles/importer/files/importer/models/rule.py +++ b/roles/importer/files/importer/models/rule.py @@ -64,8 +64,7 @@ class RuleNormalized(BaseModel): # noqa: PLW1641 rule_src_zone: str | None = None rule_dst_zone: str | None = None rule_head_text: str | None = None - xlate_rule: int | None = None - nat_rule: bool = False + xlate_rule_uid: str | None = None nat_rule: bool = False def __eq__(self, other: object) -> bool: From 2f4f3eeca13bfd18dae5b5510721cea3f205fb02 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 13 Apr 2026 16:16:57 +0200 Subject: [PATCH 23/63] fix: NAT report --- .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 1 - roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 1a864e7e25..348d670069 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -186,6 +186,5 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "time": "", "rule_type": "nat", "nat_rule": True, - "xlate_rule_uid": nat_rule["uid"] + "_translated", } return (nat_in_rule, nat_out_rule) diff --git a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs index ea52da3c58..5ee9426123 100644 --- a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs +++ b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs @@ -418,7 +418,7 @@ query natRulesReport ({paramString}) id {query.OpenRulesTable} {limitOffsetString} - where: {{ {query.RuleWhereStatement} }} + where: {{ nat_rule: {{_eq: true}}, ruleByXlateRule: {{}} {query.RuleWhereStatement} }} order_by: {{ rule_num_numeric: asc }} ) {{ mgm_id: mgm_id @@ -473,7 +473,8 @@ private static void ConstructFullQuery(DynGraphqlQuery query, ReportTemplate fil case ReportType.ComplianceReport: case ReportType.ComplianceDiffReport: case ReportType.RecertEventReport: - query.FullQuery = Queries.Compact(ConstructRulesQuery(query, paramString, filter)); + string rq = ConstructRulesQuery(query, paramString, filter); + query.FullQuery = Queries.Compact(rq); break; case ReportType.Recertification: @@ -494,7 +495,8 @@ private static void ConstructFullQuery(DynGraphqlQuery query, ReportTemplate fil break; case ReportType.NatRules: - query.FullQuery = Queries.Compact(ConstructNatRulesQuery(query, paramString, filter)); + string nrq = ConstructNatRulesQuery(query, paramString, filter); + query.FullQuery = Queries.Compact(nrq); break; case ReportType.Connections: From f694343013130faca91a405785f562e6c861027b Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 13 Apr 2026 16:20:19 +0200 Subject: [PATCH 24/63] fix: Revert change --- roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs index 5ee9426123..561115e545 100644 --- a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs +++ b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs @@ -473,8 +473,7 @@ private static void ConstructFullQuery(DynGraphqlQuery query, ReportTemplate fil case ReportType.ComplianceReport: case ReportType.ComplianceDiffReport: case ReportType.RecertEventReport: - string rq = ConstructRulesQuery(query, paramString, filter); - query.FullQuery = Queries.Compact(rq); + query.FullQuery = Queries.Compact(ConstructRulesQuery(query, paramString, filter)); break; case ReportType.Recertification: @@ -495,8 +494,7 @@ private static void ConstructFullQuery(DynGraphqlQuery query, ReportTemplate fil break; case ReportType.NatRules: - string nrq = ConstructNatRulesQuery(query, paramString, filter); - query.FullQuery = Queries.Compact(nrq); + query.FullQuery = Queries.Compact(ConstructNatRulesQuery(query, paramString, filter)); break; case ReportType.Connections: From 8d155255fec8d7e88821b854697cb7cfc3614e97 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 13 Apr 2026 17:27:51 +0200 Subject: [PATCH 25/63] fix: NAT report --- roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs index 561115e545..ed38ac2d7b 100644 --- a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs +++ b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs @@ -394,7 +394,7 @@ query natRulesReport ({paramString}) id: dev_id name: dev_name uid: dev_uid - rulebase_links(where: {{ {query.RulebaseLinkWhereStatement}, stm_link_type: {{id: {{_eq: 6}} }} }}) + rulebase_links(where: {{ {query.RulebaseLinkWhereStatement} }}) {{ linkType: stm_link_type {{ name From 57db4cf70460662300e73fc43b81617dc2e94576 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 13 Apr 2026 17:57:04 +0200 Subject: [PATCH 26/63] fix: Nat rules set as access rule --- .../importer/files/importer/fw_modules/checkpointR8x/cp_nat.py | 2 ++ .../importer/files/importer/fw_modules/checkpointR8x/cp_rule.py | 1 + .../files/importer/model_controllers/fwconfig_import_rule.py | 2 +- roles/importer/files/importer/models/rule.py | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 348d670069..c9550fa323 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -168,6 +168,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "rule_type": "access", "nat_rule": True, "xlate_rule_uid": nat_rule["uid"] + "_translated", + "access_rule": False, } nat_out_rule = { "uid": nat_rule["uid"] + "_translated", @@ -186,5 +187,6 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "time": "", "rule_type": "nat", "nat_rule": True, + "access_rule": False, } return (nat_in_rule, nat_out_rule) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py index 394f474c4f..2e264984a4 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_rule.py @@ -386,6 +386,7 @@ def parse_single_rule( "parent_rule_uid": sanitize(parent_rule_uid), "last_hit": sanitize(last_hit), "nat_rule": bool(native_rule.get("nat_rule", False)), + "access_rule": bool(native_rule.get("access_rule", True)), } if comments is not None: rule["rule_comment"] = sanitize(comments) diff --git a/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py b/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py index a0855c29c2..915ea056eb 100644 --- a/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py +++ b/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py @@ -1145,7 +1145,7 @@ def prepare_rule_for_import(self, rule: RuleNormalized, rulebase_uid: str) -> Ru rule_comment=rule.rule_comment, rule_from_zone=None, # TODO: to be removed or changed to string of joined zone names rule_to_zone=None, # TODO: to be removed or changed to string of joined zone names - access_rule=True, + access_rule=rule.access_rule, nat_rule=rule.nat_rule, is_global=False, rulebase_id=rulebase_id, diff --git a/roles/importer/files/importer/models/rule.py b/roles/importer/files/importer/models/rule.py index 961446d5aa..f04d03cdf4 100644 --- a/roles/importer/files/importer/models/rule.py +++ b/roles/importer/files/importer/models/rule.py @@ -66,6 +66,7 @@ class RuleNormalized(BaseModel): # noqa: PLW1641 rule_head_text: str | None = None xlate_rule_uid: str | None = None nat_rule: bool = False + access_rule: bool = True def __eq__(self, other: object) -> bool: if not isinstance(other, RuleNormalized): From ed5e471f32029448969555eff9b74deced922d67 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 13 Apr 2026 18:06:06 +0200 Subject: [PATCH 27/63] refactor: Revert changes and remove unused attributes --- .../importer/fw_modules/checkpointR8x/cp_nat.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index c9550fa323..34393f1647 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -14,7 +14,6 @@ def normalize_nat_rules( native_nat_rulebases = native_config.get("nat_rulebases", []) if not native_nat_rulebases: return - seen_uids: set[str] = set() for gateway in native_config["gateways"]: for nat_rulebase in native_nat_rulebases: if "nat_rule_chunks" not in nat_rulebase: @@ -51,7 +50,7 @@ def normalize_nat_rules( link for link in normalized_gateway["RulebaseLinks"] if link["to_rulebase_uid"] == normalized_nat_rulebase.uid - # and link["link_type"] == "nat" + and link["link_type"] == "nat" and link["from_rulebase_uid"] == normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"] ): normalized_gateway["RulebaseLinks"].append( @@ -84,7 +83,7 @@ def normalize_nat_rules( link for link in normalized_gateway["RulebaseLinks"] if link["to_rulebase_uid"] == section_rulebase.uid - # and link["link_type"] == "nat" + and link["link_type"] == "nat" and link["from_rulebase_uid"] == normalized_nat_rulebase.uid ): normalized_gateway["RulebaseLinks"].append( @@ -99,10 +98,6 @@ def normalize_nat_rules( ) for rule in src_rulebase["rulebase"]: - uid = rule.get("uid") - if uid in seen_uids: - continue - # seen_uids.add(uid) (rule_match, rule_xlate) = parse_nat_rule_transform(rule) parse_single_rule( rule_match, @@ -122,10 +117,6 @@ def normalize_nat_rules( ) if "rule-number" in src_rulebase: # rulebase is just a single rule (xlate rules do not count) - uid = src_rulebase["uid"] - if uid in seen_uids: - continue - # seen_uids.add(uid) (rule_match, rule_xlate) = parse_nat_rule_transform(src_rulebase) parse_single_rule( rule_match, @@ -165,7 +156,6 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "time": "", "enabled": nat_rule["enabled"], "comments": nat_rule["comments"], - "rule_type": "access", "nat_rule": True, "xlate_rule_uid": nat_rule["uid"] + "_translated", "access_rule": False, @@ -185,7 +175,6 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "service-negate": False, "install-on": nat_rule["install-on"], "time": "", - "rule_type": "nat", "nat_rule": True, "access_rule": False, } From d813b4d98070d19a6fb58318811f15cf8fffb9df Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 13 Apr 2026 18:43:38 +0200 Subject: [PATCH 28/63] wip: Fix NAT Rulebases missing --- .../fw_modules/checkpointR8x/cp_nat.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 34393f1647..787d8bd2ae 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -34,18 +34,6 @@ def normalize_nat_rules( ) continue - if len(normalized_gateway["RulebaseLinks"]) == 0: - normalized_gateway["RulebaseLinks"].append( - { - "from_rulebase_uid": normalized_gateway["Uid"], - "to_rulebase_uid": normalized_nat_rulebase.uid, - "link_type": "nat", - "is_initial": True, - "is_global": False, - "is_section": False, - } - ) - if not any( link for link in normalized_gateway["RulebaseLinks"] @@ -57,7 +45,7 @@ def normalize_nat_rules( { "from_rulebase_uid": normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], "to_rulebase_uid": normalized_nat_rulebase.uid, - "link_type": "nat", + "link_type": "ordered", "is_initial": False, "is_global": False, "is_section": False, @@ -90,7 +78,7 @@ def normalize_nat_rules( { "from_rulebase_uid": normalized_nat_rulebase.uid, "to_rulebase_uid": section_rulebase.uid, - "link_type": "nat", + "link_type": "concatenated", "is_initial": False, "is_global": False, "is_section": False, @@ -141,12 +129,12 @@ def normalize_nat_rules( def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: nat_in_rule = { - "uid": nat_rule["uid"] + "_original", + "uid": nat_rule["uid"], "source": [nat_rule["original-source"]], "destination": [nat_rule["original-destination"]], "service": [nat_rule["original-service"]], "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "_original-action"}], - "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "_original"}], + "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"]}], "type": "nat", "rule-number": 0, "source-negate": False, From efecb081b1715513f888636f29512901e9c30223 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Tue, 14 Apr 2026 19:39:03 +0200 Subject: [PATCH 29/63] fix: Rulebase order --- .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 787d8bd2ae..2ebd915b5e 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -26,6 +26,9 @@ def normalize_nat_rules( rules={}, ) + if not any(rb for rb in normalized_config["policies"] if rb.uid == normalized_nat_rulebase.uid): + normalized_config["policies"].append(normalized_nat_rulebase) + normalized_gateway = next((gw for gw in normalized_config["gateways"] if gw["Uid"] == gateway["uid"]), None) if normalized_gateway is None: @@ -123,9 +126,6 @@ def normalize_nat_rules( native_config["policies"], ) - if not any(rb for rb in normalized_config["policies"] if rb.uid == normalized_nat_rulebase.uid): - normalized_config["policies"].append(normalized_nat_rulebase) - def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: nat_in_rule = { From b202e5391b4b999a0a31698f45178b7a6c2fa73d Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Tue, 14 Apr 2026 19:40:07 +0200 Subject: [PATCH 30/63] fix: Format --- .../files/importer/fw_modules/checkpointR8x/fwcommon.py | 1 - .../importer/model_controllers/fwconfig_import_rule.py | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index 1af07a5839..0ce3b67dee 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -361,7 +361,6 @@ def process_devices( if not device_config: continue found_gateway = next((gw for gw in gateways_and_servers if gw["uid"] == device["uid"]), None) - #found_gateway = next((gw for gw in gateways_and_servers if gw["name"] == "CP_SMS_Phys_GWSingle_Test4FWO"), None) if found_gateway is None: FWOLogger.warning("Could not find gateway for device, skipping: " + str(device["uid"])) native_config_domain["gateways"].append(device_config) diff --git a/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py b/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py index 915ea056eb..be35496921 100644 --- a/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py +++ b/roles/importer/files/importer/model_controllers/fwconfig_import_rule.py @@ -115,7 +115,8 @@ def update_rulebase_diffs(self, prev_config: FwConfigNormalized) -> None: num_inserted_rules, inserted_rule_ids = self.add_new_rules( { rule_uid: (curr_rules[rule_uid], curr_rule_to_rulebase[rule_uid]) - for rule_uid in (added_rule_uids | changed_rule_uids) if curr_rules[rule_uid].xlate_rule_uid is None + for rule_uid in (added_rule_uids | changed_rule_uids) + if curr_rules[rule_uid].xlate_rule_uid is None } ) self.uid2id_mapper.add_rule_mappings(inserted_rule_ids) @@ -125,7 +126,8 @@ def update_rulebase_diffs(self, prev_config: FwConfigNormalized) -> None: num_inserted_nat_rules, inserted_nat_rule_ids = self.add_new_rules( { rule_uid: (curr_rules[rule_uid], curr_rule_to_rulebase[rule_uid]) - for rule_uid in (added_rule_uids | changed_rule_uids) if curr_rules[rule_uid].xlate_rule_uid is not None + for rule_uid in (added_rule_uids | changed_rule_uids) + if curr_rules[rule_uid].xlate_rule_uid is not None } ) num_inserted_rules += num_inserted_nat_rules From 6429a858c18b022b4e2365f7c7626f28264c7664 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Tue, 14 Apr 2026 20:07:54 +0200 Subject: [PATCH 31/63] refactor: Satisfy Sonarqube --- .../fw_modules/checkpointR8x/cp_nat.py | 270 +++++++++++------- .../fw_modules/checkpointR8x/fwcommon.py | 56 ++-- 2 files changed, 194 insertions(+), 132 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 2ebd915b5e..c59e332915 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -14,117 +14,171 @@ def normalize_nat_rules( native_nat_rulebases = native_config.get("nat_rulebases", []) if not native_nat_rulebases: return + for gateway in native_config["gateways"]: - for nat_rulebase in native_nat_rulebases: - if "nat_rule_chunks" not in nat_rulebase: - continue - - normalized_nat_rulebase = Rulebase( - uid="nat-rulebase-" + gateway["uid"], - mgm_uid=import_state.mgm_details.uid, - name="NAT", - rules={}, + parse_native_nat_rulebases(gateway, native_nat_rulebases, import_state, normalized_config, native_config) + + +def parse_native_nat_rulebases( + gateway: dict[str, Any], + native_nat_rulebases: list[dict[str, Any]], + import_state: ImportState, + normalized_config: dict[str, Any], + native_config: dict[str, Any], +): + for nat_rulebase in native_nat_rulebases: + if "nat_rule_chunks" not in nat_rulebase: + continue + + normalized_nat_rulebase = insert_parent_nat_rulebase(gateway, import_state, normalized_config) + normalized_gateway = next((gw for gw in normalized_config["gateways"] if gw["Uid"] == gateway["uid"]), None) + + if normalized_gateway is None: + FWOLogger.warning("Could not find normalized gateway for NAT rulebase, skipping: " + str(gateway["uid"])) + continue + + insert_rulebase_link( + from_rulebase_uid=normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], + to_rulebase_uid=normalized_nat_rulebase.uid, + link_type="ordered", + normalized_gateway=normalized_gateway, + ) + + for chunk in nat_rulebase["nat_rule_chunks"]: + parse_nat_rule_chunk( + chunk, + normalized_nat_rulebase, + gateway, + native_config, + import_state, + normalized_config, + normalized_gateway, ) - if not any(rb for rb in normalized_config["policies"] if rb.uid == normalized_nat_rulebase.uid): - normalized_config["policies"].append(normalized_nat_rulebase) - - normalized_gateway = next((gw for gw in normalized_config["gateways"] if gw["Uid"] == gateway["uid"]), None) - - if normalized_gateway is None: - FWOLogger.warning( - "Could not find normalized gateway for NAT rulebase, skipping: " + str(gateway["uid"]) - ) - continue - - if not any( - link - for link in normalized_gateway["RulebaseLinks"] - if link["to_rulebase_uid"] == normalized_nat_rulebase.uid - and link["link_type"] == "nat" - and link["from_rulebase_uid"] == normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"] - ): - normalized_gateway["RulebaseLinks"].append( - { - "from_rulebase_uid": normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], - "to_rulebase_uid": normalized_nat_rulebase.uid, - "link_type": "ordered", - "is_initial": False, - "is_global": False, - "is_section": False, - } - ) - - for chunk in nat_rulebase["nat_rule_chunks"]: - if "rulebase" not in chunk: - continue - for src_rulebase in chunk["rulebase"]: - if "rulebase" in src_rulebase: - section_rulebase = Rulebase( - uid=src_rulebase["uid"], - mgm_uid=import_state.mgm_details.uid, - name=src_rulebase["name"], - rules={}, - ) - - if not any(rb for rb in normalized_config["policies"] if rb.uid == section_rulebase.uid): - normalized_config["policies"].append(section_rulebase) - - if not any( - link - for link in normalized_gateway["RulebaseLinks"] - if link["to_rulebase_uid"] == section_rulebase.uid - and link["link_type"] == "nat" - and link["from_rulebase_uid"] == normalized_nat_rulebase.uid - ): - normalized_gateway["RulebaseLinks"].append( - { - "from_rulebase_uid": normalized_nat_rulebase.uid, - "to_rulebase_uid": section_rulebase.uid, - "link_type": "concatenated", - "is_initial": False, - "is_global": False, - "is_section": False, - } - ) - - for rule in src_rulebase["rulebase"]: - (rule_match, rule_xlate) = parse_nat_rule_transform(rule) - parse_single_rule( - rule_match, - section_rulebase, - section_rulebase.name, - section_rulebase.uid, - gateway, - native_config["policies"], - ) - parse_single_rule( # do not increase rule_num here - rule_xlate, - section_rulebase, - section_rulebase.name, - section_rulebase.uid, - gateway, - native_config["policies"], - ) - - if "rule-number" in src_rulebase: # rulebase is just a single rule (xlate rules do not count) - (rule_match, rule_xlate) = parse_nat_rule_transform(src_rulebase) - parse_single_rule( - rule_match, - normalized_nat_rulebase, - normalized_nat_rulebase.name, - normalized_nat_rulebase.uid, - gateway, - native_config["policies"], - ) - parse_single_rule( # do not increase rule_num here (xlate rules do not count) - rule_xlate, - normalized_nat_rulebase, - normalized_nat_rulebase.name, - normalized_nat_rulebase.uid, - gateway, - native_config["policies"], - ) + +def insert_parent_nat_rulebase( + gateway: dict[str, Any], + import_state: ImportState, + normalized_config: dict[str, Any], +) -> Rulebase: + normalized_nat_rulebase = Rulebase( + uid="nat-rulebase-" + gateway["uid"], + mgm_uid=import_state.mgm_details.uid, + name="NAT", + rules={}, + ) + + if not any(rb for rb in normalized_config["policies"] if rb.uid == normalized_nat_rulebase.uid): + normalized_config["policies"].append(normalized_nat_rulebase) + + return normalized_nat_rulebase + + +def insert_rulebase_link( + from_rulebase_uid: str, + to_rulebase_uid: str, + link_type: str, + normalized_gateway: dict[str, Any], +) -> None: + if not any( + link + for link in normalized_gateway["RulebaseLinks"] + if link["to_rulebase_uid"] == to_rulebase_uid + and link["link_type"] == link_type + and link["from_rulebase_uid"] == from_rulebase_uid + ): + normalized_gateway["RulebaseLinks"].append( + { + "from_rulebase_uid": from_rulebase_uid, + "to_rulebase_uid": to_rulebase_uid, + "link_type": link_type, + "is_initial": False, + "is_global": False, + "is_section": False, + } + ) + + +def parse_nat_rulebase( + src_rulebase: dict[str, Any], + normalized_nat_rulebase: Rulebase, + gateway: dict[str, Any], + native_config: dict[str, Any], + import_state: ImportState, + normalized_config: dict[str, Any], + normalized_gateway: dict[str, Any], +): + section_rulebase = Rulebase( + uid=src_rulebase["uid"], + mgm_uid=import_state.mgm_details.uid, + name=src_rulebase["name"], + rules={}, + ) + + if not any(rb for rb in normalized_config["policies"] if rb.uid == section_rulebase.uid): + normalized_config["policies"].append(section_rulebase) + + insert_rulebase_link( + from_rulebase_uid=normalized_nat_rulebase.uid, + to_rulebase_uid=section_rulebase.uid, + link_type="concatenated", + normalized_gateway=normalized_gateway, + ) + + for rule in src_rulebase["rulebase"]: + parse_nat_rule(rule, section_rulebase, gateway, native_config) + + +def parse_nat_rule( + src_rulebase: dict[str, Any], + rulebase: Rulebase, + gateway: dict[str, Any], + native_config: dict[str, Any], +): + (rule_match, rule_xlate) = parse_nat_rule_transform(src_rulebase) + parse_single_rule( + rule_match, + rulebase, + rulebase.name, + rulebase.uid, + gateway, + native_config["policies"], + ) + parse_single_rule( # do not increase rule_num here (xlate rules do not count) + rule_xlate, + rulebase, + rulebase.name, + rulebase.uid, + gateway, + native_config["policies"], + ) + + +def parse_nat_rule_chunk( + chunk: dict[str, Any], + normalized_nat_rulebase: Rulebase, + gateway: dict[str, Any], + native_config: dict[str, Any], + import_state: ImportState, + normalized_config: dict[str, Any], + normalized_gateway: dict[str, Any], +): + if "rulebase" not in chunk: + return + + for src_rulebase in chunk["rulebase"]: + if "rulebase" in src_rulebase: + parse_nat_rulebase( + src_rulebase, + normalized_nat_rulebase, + gateway, + native_config, + import_state, + normalized_config, + normalized_gateway, + ) + if "rule-number" in src_rulebase: # rulebase is just a single rule (xlate rules do not count) + parse_nat_rule(src_rulebase, normalized_nat_rulebase, gateway, native_config) def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index 0ce3b67dee..2838e01f44 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -340,6 +340,35 @@ def handle_super_manager( return global_assignments, global_policy_structure, global_domain, global_sid +def get_policy_for_device( + device: dict[str, Any], + gateways_and_servers: list[dict[str, Any]], + policy_structure: list[dict[str, Any]], +) -> dict[str, Any] | None: + found_gateway = next((gw for gw in gateways_and_servers if gw["uid"] == device["uid"]), None) + if found_gateway is None: + FWOLogger.warning("Could not find gateway for device, skipping: " + str(device["uid"])) + return None + + if "policy" not in found_gateway: + FWOLogger.warning("Could not find policy in gateway, skipping: " + str(device["uid"])) + return None + + gateway_policy = found_gateway["policy"] + if "access-policy-name" not in gateway_policy: + FWOLogger.warning("Could not find access policy for gateway, skipping: " + str(device["uid"])) + return None + + policy = next( + (policy for policy in policy_structure if policy["name"] == gateway_policy["access-policy-name"]), None + ) + + if not policy: + return None + + return policy + + def process_devices( manager_details: ManagementController, policy_structure: list[dict[str, Any]], @@ -357,35 +386,14 @@ def process_devices( for device in manager_details.devices: if device["importDisabled"] and not import_state.force_import: continue + device_config: dict[str, Any] = initialize_device_config(device) if not device_config: continue - found_gateway = next((gw for gw in gateways_and_servers if gw["uid"] == device["uid"]), None) - if found_gateway is None: - FWOLogger.warning("Could not find gateway for device, skipping: " + str(device["uid"])) - native_config_domain["gateways"].append(device_config) - continue - - if "policy" not in found_gateway: - FWOLogger.warning("Could not find policy for gateway, skipping: " + str(device["uid"])) - native_config_domain["gateways"].append(device_config) - continue - - gateway_policy = found_gateway["policy"] - if "access-policy-name" not in gateway_policy: - FWOLogger.warning("Could not find access policy for gateway, skipping: " + str(device["uid"])) - native_config_domain["gateways"].append(device_config) - continue - - policy = next( - (policy for policy in policy_structure if policy["name"] == gateway_policy["access-policy-name"]), None - ) + policy = get_policy_for_device(device, gateways_and_servers, policy_structure) if not policy: - FWOLogger.warning( - "Could not find policy structure for gateway policy, skipping: " - + str(gateway_policy["access-policy-name"]) - ) + FWOLogger.warning("Could not find policy structure for device, skipping: " + str(device["uid"])) native_config_domain["gateways"].append(device_config) continue From acf5cd86301286b940ce10eea71849fa51a6d4aa Mon Sep 17 00:00:00 2001 From: ErikPre Date: Wed, 15 Apr 2026 08:53:56 +0200 Subject: [PATCH 32/63] feat: copilot comment, initial check --- .../fw_modules/checkpointR8x/cp_nat.py | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index c59e332915..5c0ebb8336 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -19,6 +19,29 @@ def normalize_nat_rules( parse_native_nat_rulebases(gateway, native_nat_rulebases, import_state, normalized_config, native_config) +def get_initial_nat_rulebase_link(gateway: dict[str, Any], normalized_config: dict[str, Any]) -> dict[str, Any] | None: + normalized_gateway = next((gw for gw in normalized_config["gateways"] if gw["Uid"] == gateway["uid"]), None) + + if normalized_gateway is None: + FWOLogger.warning("Could not find normalized gateway for initial NAT rulebase link: " + str(gateway["uid"])) + return None + + initial_gateway_link = next( + ( + link + for link in normalized_gateway["RulebaseLinks"] + if link.get("is_initial") and link.get("link_type") == "ordered" + ), + None, + ) + + if initial_gateway_link is None: + FWOLogger.warning("Could not find initial gateway rulebase link for NAT rulebase: " + str(gateway["uid"])) + return None + + return initial_gateway_link + + def parse_native_nat_rulebases( gateway: dict[str, Any], native_nat_rulebases: list[dict[str, Any]], @@ -37,8 +60,21 @@ def parse_native_nat_rulebases( FWOLogger.warning("Could not find normalized gateway for NAT rulebase, skipping: " + str(gateway["uid"])) continue + initial_gateway_link = get_initial_nat_rulebase_link(gateway, normalized_config) + + if initial_gateway_link is None: + continue + + initial_to_rulebase_uid = initial_gateway_link.get("to_rulebase_uid") + if not initial_to_rulebase_uid: + FWOLogger.warning( + "Initial gateway rulebase link is missing to_rulebase_uid for NAT rulebase, skipping: " + + str(gateway["uid"]) + ) + continue + insert_rulebase_link( - from_rulebase_uid=normalized_gateway["RulebaseLinks"][0]["to_rulebase_uid"], + from_rulebase_uid=initial_to_rulebase_uid, to_rulebase_uid=normalized_nat_rulebase.uid, link_type="ordered", normalized_gateway=normalized_gateway, From 9bc72fa0b1407610b9ea3430985869c53e64b6f6 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Wed, 15 Apr 2026 09:08:11 +0200 Subject: [PATCH 33/63] feat: set simpler standard --- .../files/importer/fw_modules/checkpointR8x/cp_getter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py index 0bef8b3cba..ea1a9a81f9 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py @@ -981,7 +981,7 @@ def get_object_details_from_api(uid_missing_obj: str, sid: str = "", apiurl: str def get_gateways_and_servers(sid: str = "", apiurl: str = "") -> list[dict[str, Any]]: """Fetch gateways and servers from the API.""" current = 0 - total = current + 1 + total = 1 gateways_and_servers: list[dict[str, Any]] = [] From e58a152f9d90671ca1a73268985f46a3f1e890e7 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Wed, 15 Apr 2026 09:29:28 +0200 Subject: [PATCH 34/63] fix: pagination of gateways and servers --- .../files/importer/fw_modules/checkpointR8x/cp_getter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py index ea1a9a81f9..3aee30207d 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_getter.py @@ -987,7 +987,7 @@ def get_gateways_and_servers(sid: str = "", apiurl: str = "") -> list[dict[str, while current < total: try: - result = cp_api_call(apiurl, "show-gateways-and-servers", {"details-level": "full"}, sid) + result = cp_api_call(apiurl, "show-gateways-and-servers", {"details-level": "full", "offset": current}, sid) except Exception as e: raise FwoImporterError(f"error while trying to get gateways and servers: {e}") From f396510b582bb9e7c4b4c1fc321d87b9ed102601 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Thu, 16 Apr 2026 17:18:19 +0200 Subject: [PATCH 35/63] wip: Review comments & new link type --- .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 8 ++++---- .../files/importer/fw_modules/checkpointR8x/fwcommon.py | 7 +------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 5c0ebb8336..7769097f8d 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -76,7 +76,7 @@ def parse_native_nat_rulebases( insert_rulebase_link( from_rulebase_uid=initial_to_rulebase_uid, to_rulebase_uid=normalized_nat_rulebase.uid, - link_type="ordered", + link_type="nat", normalized_gateway=normalized_gateway, ) @@ -157,7 +157,7 @@ def parse_nat_rulebase( insert_rulebase_link( from_rulebase_uid=normalized_nat_rulebase.uid, to_rulebase_uid=section_rulebase.uid, - link_type="concatenated", + link_type="nat", normalized_gateway=normalized_gateway, ) @@ -225,7 +225,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "service": [nat_rule["original-service"]], "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "_original-action"}], "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"]}], - "type": "nat", + "rule_type": "nat", "rule-number": 0, "source-negate": False, "destination-negate": False, @@ -245,7 +245,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "service": [nat_rule["translated-service"]], "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "_translated-action"}], "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "_translated"}], - "type": "nat", + "rule_type": "nat", "rule-number": 0, "enabled": True, "source-negate": False, diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index 2838e01f44..8ef7975c4d 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -359,15 +359,10 @@ def get_policy_for_device( FWOLogger.warning("Could not find access policy for gateway, skipping: " + str(device["uid"])) return None - policy = next( + return next( (policy for policy in policy_structure if policy["name"] == gateway_policy["access-policy-name"]), None ) - if not policy: - return None - - return policy - def process_devices( manager_details: ManagementController, From 97103f9c1c9cadb2023d4f2b16c4d92010b4db22 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Mon, 20 Apr 2026 19:39:41 +0200 Subject: [PATCH 36/63] feat: redo rule_type to type --- agents | 1 + .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 160000 agents diff --git a/agents b/agents new file mode 160000 index 0000000000..a5ed1716a9 --- /dev/null +++ b/agents @@ -0,0 +1 @@ +Subproject commit a5ed1716a99d6f4d31be3f3889f6b2e4916b417c diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 7769097f8d..06fcf720ca 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -225,7 +225,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "service": [nat_rule["original-service"]], "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "_original-action"}], "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"]}], - "rule_type": "nat", + "type": "nat", "rule-number": 0, "source-negate": False, "destination-negate": False, @@ -245,7 +245,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "service": [nat_rule["translated-service"]], "action": [{"name": "accept", "type": "nat-action", "uid": nat_rule["uid"] + "_translated-action"}], "track": [{"type": "nat", "name": "None", "uid": nat_rule["uid"] + "_translated"}], - "rule_type": "nat", + "type": "nat", "rule-number": 0, "enabled": True, "source-negate": False, From 268a02b22581a71807420b2b674097d802b283d0 Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Tue, 21 Apr 2026 16:02:57 +0200 Subject: [PATCH 37/63] fix: Query --- roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs index c86ed1a7ad..4368497ace 100644 --- a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs +++ b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs @@ -17,6 +17,7 @@ public class DynGraphqlQuery(string rawInput) public Dictionary QueryVariables { get; set; } = []; public string FullQuery { get; set; } = ""; public string RulebaseLinkWhereStatement { get; set; } = ""; + public string NatRulebaseLinkWhereStatement { get; set; } = ""; public string RuleWhereStatement { get; set; } = ""; public string NwObjWhereStatement { get; set; } = ""; public string SvcObjWhereStatement { get; set; } = ""; @@ -394,7 +395,7 @@ query natRulesReport ({paramString}) id: dev_id name: dev_name uid: dev_uid - rulebase_links(where: {{ {query.RulebaseLinkWhereStatement} }}) + rulebase_links(where: {{ {query.NatRulebaseLinkWhereStatement} }}) {{ linkType: stm_link_type {{ name @@ -794,9 +795,14 @@ private static void SetTimeFilter(ref DynGraphqlQuery query, TimeFilter? timeFil case ReportType.RecertEventReport: query.QueryParameters.Add("$import_id_start: bigint "); query.QueryParameters.Add("$import_id_end: bigint "); + String removedStatement = $"_or: [{{removed: {{_gt: $import_id_start}} }}, {{removed: {{_is_null: true}} }}]"; query.RulebaseLinkWhereStatement += $"created: {{_lte: $import_id_end }}" + - $"_or: [{{removed: {{_gt: $import_id_start}} }}, {{removed: {{_is_null: true}} }}]"; + removedStatement + + $"stm_link_type: {{id: {{_neq: 6}}}}"; // Filter out NAT rulebase links + query.NatRulebaseLinkWhereStatement += + $"created: {{_lte: $import_id_end }}" + + $"_or: [{{is_initial: {{_eq: true}}, {removedStatement}}}, {{link_type: {{_eq: 6}}, {removedStatement}}}]"; query.RuleWhereStatement += $"rule_create: {{_lte: $import_id_end}}" + $"_or: [{{removed: {{_gt: $import_id_start}} }}, {{removed: {{_is_null: true}} }}]"; From 4f3641366f278608d44d6d5f1f35219ea9173f0d Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Tue, 21 Apr 2026 16:04:34 +0200 Subject: [PATCH 38/63] wip: Rule tree builder --- .../files/FWO.Services/RuleTreeBuilder/RuleTreeBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roles/lib/files/FWO.Services/RuleTreeBuilder/RuleTreeBuilder.cs b/roles/lib/files/FWO.Services/RuleTreeBuilder/RuleTreeBuilder.cs index ae69c506c8..a88e670d63 100644 --- a/roles/lib/files/FWO.Services/RuleTreeBuilder/RuleTreeBuilder.cs +++ b/roles/lib/files/FWO.Services/RuleTreeBuilder/RuleTreeBuilder.cs @@ -140,7 +140,7 @@ public List ProcessLink(RulebaseLink link, List? trail = null) { trail = ProcessSectionLink(link, rulebase, trail); } - else if (link.LinkType == 4) + else if (link.LinkType == 4 || link.LinkType == 6) { trail = ProcessConcatenationLink(link, rulebase, trail); } @@ -336,7 +336,7 @@ private Rule GetUniqueRuleObject(Rule rule) private List EnsureTrailStartsWithZeroForFirstRule(RulebaseLink link, RuleTreeItem lastAddedItem, List trail, int index) { if (index == 0 - && (!(link.LinkType == 4) + && (link.LinkType != 4 && link.LinkType != 6 || lastAddedItem.Parent?.Children.Count() == 1)) { trail = trail.ToList(); @@ -394,7 +394,7 @@ private void SetParentForTreeItem(RuleTreeItem item, RulebaseLink link, RuleTree { SetParentForTreeItem(RuleTree, item); } - else if (link.LinkType == 4 && lastAddedItem.Parent != null) + else if ((link.LinkType == 4 || link.LinkType == 6) && lastAddedItem.Parent != null) { SetParentForTreeItem((RuleTreeItem)lastAddedItem.Parent, item); } From 074856c368d5f6f0b8bd2567adc8c0dc35ef92af Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Tue, 21 Apr 2026 16:06:52 +0200 Subject: [PATCH 39/63] fix: Format --- .../files/importer/fw_modules/checkpointR8x/fwcommon.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index 8ef7975c4d..ee04377135 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -359,9 +359,7 @@ def get_policy_for_device( FWOLogger.warning("Could not find access policy for gateway, skipping: " + str(device["uid"])) return None - return next( - (policy for policy in policy_structure if policy["name"] == gateway_policy["access-policy-name"]), None - ) + return next((policy for policy in policy_structure if policy["name"] == gateway_policy["access-policy-name"]), None) def process_devices( From 4f10be6dca62eb7d5008231670e8dcb02ea024fb Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 27 Apr 2026 20:21:43 +0200 Subject: [PATCH 40/63] fix: Review comments --- roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs index 89237a650f..25a95f1e92 100644 --- a/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs +++ b/roles/lib/files/FWO.Report.Filter/DynGraphqlQuery.cs @@ -801,11 +801,11 @@ private static void SetTimeFilter(ref DynGraphqlQuery query, TimeFilter? timeFil case ReportType.RecertEventReport: query.QueryParameters.Add("$import_id_start: bigint "); query.QueryParameters.Add("$import_id_end: bigint "); - String removedStatement = $"_or: [{{removed: {{_gt: $import_id_start}} }}, {{removed: {{_is_null: true}} }}]"; + string removedStatement = $"_or: [{{removed: {{_gt: $import_id_start}} }}, {{removed: {{_is_null: true}} }}]"; query.RulebaseLinkWhereStatement += $"created: {{_lte: $import_id_end }}" + removedStatement + - $"stm_link_type: {{id: {{_neq: 6}}}}"; // Filter out NAT rulebase links + $" stm_link_type: {{id: {{_neq: 6}}}}"; // Filter out NAT rulebase links query.NatRulebaseLinkWhereStatement += $"created: {{_lte: $import_id_end }}" + $"_or: [{{is_initial: {{_eq: true}}, {removedStatement}}}, {{link_type: {{_eq: 6}}, {removedStatement}}}]"; From 8e8e7f7022da4012a5f7b994f5c08ffa806115ce Mon Sep 17 00:00:00 2001 From: Lennart Schmidt Date: Mon, 27 Apr 2026 20:38:32 +0200 Subject: [PATCH 41/63] fix: Review comments --- .../fw_modules/checkpointR8x/fwcommon.py | 24 ++++++++++--------- .../test/test_checkpoint_file_import.py | 15 ++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py index ee04377135..f1ebb68f7d 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/fwcommon.py @@ -659,17 +659,19 @@ def get_ordered_layer_uids(policy: dict[str, Any], device_config: dict[str, Any] """ Get UIDs of ordered layers for policy of device """ - ordered_layer_uids: list[str] = [policy["uid"]] - for target in policy["targets"]: - if target["uid"] == device_config["uid"] or target["uid"] == "all": - ordered_layer_uids.extend( - [ - access_layer["uid"] - for access_layer in policy["access-layers"] - if access_layer["domain"] == domain or domain == "" - ] - ) - return ordered_layer_uids + ordered_layer_uids: list[str] = [policy["uid"]] if "uid" in policy else [] + + is_targeted = any(target["uid"] == device_config["uid"] or target["uid"] == "all" for target in policy["targets"]) + if is_targeted: + ordered_layer_uids.extend( + [ + access_layer["uid"] + for access_layer in policy["access-layers"] + if access_layer["domain"] == domain or domain == "" + ] + ) + + return list(dict.fromkeys(ordered_layer_uids)) def get_objects(native_config_dict: dict[str, Any], import_state: ImportState) -> int: diff --git a/roles/importer/files/importer/test/test_checkpoint_file_import.py b/roles/importer/files/importer/test/test_checkpoint_file_import.py index 23f773ddc8..b85b82b5b2 100644 --- a/roles/importer/files/importer/test/test_checkpoint_file_import.py +++ b/roles/importer/files/importer/test/test_checkpoint_file_import.py @@ -35,3 +35,18 @@ def test_checkpoint_native_file_import_skips_login(self, import_state_controller _, result = fwcommon.get_config(config_in, import_state) assert result is config_in + + def test_get_ordered_layer_uids_adds_access_layers_once_for_multiple_matching_targets(self): + policy = { + "uid": "policy-uid", + "targets": [{"uid": "device-uid"}, {"uid": "all"}], + "access-layers": [ + {"uid": "layer-1", "domain": "domain-a"}, + {"uid": "layer-2", "domain": "domain-a"}, + ], + } + device_config = {"uid": "device-uid"} + + ordered_layer_uids = fwcommon.get_ordered_layer_uids(policy, device_config, "domain-a") + + assert ordered_layer_uids == ["policy-uid", "layer-1", "layer-2"] From cde5db7ca7b6c0c989f1ed890b95c05d7a1b4c2b Mon Sep 17 00:00:00 2001 From: Lennart Schmidt <60012921+Laennart@users.noreply.github.com> Date: Fri, 22 May 2026 11:07:26 +0200 Subject: [PATCH 42/63] feat: Forti NAT Support --- ...anagementForLatestNormalizedConfig.graphql | 5 + .../getManagementForNormalizedConfig.graphql | 5 + .../fw_modules/fortiadom5ff/fmgr_rule.py | 528 ++++++++++++++++-- .../files/importer/test/test_fortiadom5ff.py | 91 ++- roles/lib/files/FWO.Data/NormalizedRule.cs | 14 +- roles/lib/files/FWO.Data/Rule.cs | 12 + roles/tests-unit/files/FWO.Test/ExportTest.cs | 4 +- .../tests-unit/files/FWO.Test/RuleDataTest.cs | 39 ++ 8 files changed, 660 insertions(+), 38 deletions(-) diff --git a/roles/common/files/fwo-api-calls/report/getManagementForLatestNormalizedConfig.graphql b/roles/common/files/fwo-api-calls/report/getManagementForLatestNormalizedConfig.graphql index 0bd6e15001..b6c049a3d8 100644 --- a/roles/common/files/fwo-api-calls/report/getManagementForLatestNormalizedConfig.graphql +++ b/roles/common/files/fwo-api-calls/report/getManagementForLatestNormalizedConfig.graphql @@ -121,6 +121,8 @@ fragment ruleFragment on rule { rule_custom_fields rule_implied nat_rule + access_rule + xlate_rule uiuser { uiuser_username } @@ -131,6 +133,9 @@ fragment ruleFragment on rule { rule_last_hit } rule_comment + ruleByXlateRule { + rule_uid + } rule_from_zones { zone { zone_name diff --git a/roles/common/files/fwo-api-calls/report/getManagementForNormalizedConfig.graphql b/roles/common/files/fwo-api-calls/report/getManagementForNormalizedConfig.graphql index 33a55fe52d..42480a7a88 100644 --- a/roles/common/files/fwo-api-calls/report/getManagementForNormalizedConfig.graphql +++ b/roles/common/files/fwo-api-calls/report/getManagementForNormalizedConfig.graphql @@ -176,6 +176,8 @@ fragment ruleFragment on rule { rule_custom_fields rule_implied nat_rule + access_rule + xlate_rule uiuser { uiuser_username } @@ -186,6 +188,9 @@ fragment ruleFragment on rule { rule_last_hit } rule_comment + ruleByXlateRule { + rule_uid + } rule_from_zones { zone { zone_name diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index 99c63be7b7..f3261812f4 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -1,5 +1,6 @@ import copy import ipaddress +import json from datetime import datetime, timezone from typing import Any @@ -7,7 +8,10 @@ from fw_modules.fortiadom5ff.fmgr_consts import nat_types from fw_modules.fortiadom5ff.fmgr_zone import find_zones_in_normalized_config from fwo_const import LIST_DELIMITER -from fwo_exceptions import FwoDeviceWithoutLocalPackageError, FwoImporterErrorInconsistenciesError +from fwo_exceptions import ( + FwoDeviceWithoutLocalPackageError, + FwoImporterErrorInconsistenciesError, +) from fwo_log import FWOLogger from models.rule import RuleAction, RuleNormalized, RuleTrack, RuleType from models.rulebase import Rulebase @@ -16,11 +20,21 @@ STRING_PKG = "/pkg/" STRING_PM_CONFIG_GLOBAL_PKG = "/pm/config/global/pkg/" STRING_PM_CONFIG_ADOM = "/pm/config/adom/" -rule_access_scope_v4 = ["rules_global_header_v4", "rules_adom_v4", "rules_global_footer_v4"] -rule_access_scope_v6 = ["rules_global_header_v6", "rules_adom_v6", "rules_global_footer_v6"] +rule_access_scope_v4 = [ + "rules_global_header_v4", + "rules_adom_v4", + "rules_global_footer_v4", +] +rule_access_scope_v6 = [ + "rules_global_header_v6", + "rules_adom_v6", + "rules_global_footer_v6", +] rule_access_scope = rule_access_scope_v6 + rule_access_scope_v4 rule_nat_scope = ["rules_global_nat", "rules_adom_nat"] rule_scope = rule_access_scope + rule_nat_scope +ip_v4_type = 4 +ip_v6_type = 6 def normalize_rulebases( @@ -61,7 +75,13 @@ def normalize_rulebases_for_each_link_destination( normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any], ): - for rulebase_link in gateway["rulebase_links"]: + # Iterate over a snapshot because we may append NAT links while processing. + for rulebase_link in list(gateway["rulebase_links"]): + link_type = rulebase_link.get("link_type", rulebase_link.get("type", "ordered")) + if link_type == "nat": + # NAT links are generated during normalization and do not exist in native rulebases. + continue + if rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "": rulebase_to_parse = find_rulebase_to_parse(native_config["rulebases"], rulebase_link["to_rulebase_uid"]) # search in global rulebase @@ -90,8 +110,38 @@ def normalize_rulebases_for_each_link_destination( else: normalized_config_adom["policies"].append(normalized_rulebase) + # Process NAT rules from the same rulebase + has_nat_rules = any( + any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) + for native_rule in rulebase_to_parse.get("data", []) + ) + + if has_nat_rules: + # Create NAT rulebase and link + normalized_nat_rulebase = insert_parent_nat_rulebase( + normalized_config_adom, + normalized_config_global, + normalized_rulebase.uid, + normalized_rulebase.mgm_uid, + ) + + # Create RulebaseLink from access rulebase to NAT rulebase + insert_nat_rulebase_link( + from_rulebase_uid=normalized_rulebase.uid, + to_rulebase_uid=normalized_nat_rulebase.uid, + gateway=gateway, + ) + + # Parse NAT rules into the NAT rulebase + parse_nat_rules_in_rulebase( + normalized_config_adom, + normalized_config_global, + rulebase_to_parse, + normalized_nat_rulebase, + ) + # normalizing nat rulebases is work in progress - # normalize_nat_rulebase(rulebase_link, native_config, normalized_config_adom, normalized_config_global) # noqa: ERA001 + normalize_nat_rulebase(rulebase_link, native_config, normalized_config_adom, normalized_config_global) def normalize_nat_rulebase( @@ -100,11 +150,23 @@ def normalize_nat_rulebase( normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any], ): + normalized_config_adom["nat_policies"] = [] + link_type = rulebase_link.get("link_type", rulebase_link.get("type", "ordered")) + if link_type == "nat": + return + if not rulebase_link["is_section"]: for nat_type in nat_types: nat_type_string = nat_type + "_" + rulebase_link["to_rulebase_uid"] nat_rulebase = get_native_nat_rulebase(native_config, nat_type_string) - parse_nat_rulebase(nat_rulebase, nat_type_string, normalized_config_adom, normalized_config_global) + parse_nat_rulebase( + nat_rulebase, + nat_type_string, + normalized_config_adom, + normalized_config_global, + ) + + normalized_config_adom["nat_policies"].extend(nat_rulebase) # pyright: ignore[reportUnknownMemberType] def get_native_nat_rulebase(native_config: dict[str, Any], nat_type_string: str) -> list[dict[str, Any]]: @@ -122,6 +184,246 @@ def find_rulebase_to_parse(rulebase_list: list[dict[str, Any]], rulebase_uid: st return {} +def insert_parent_nat_rulebase( + normalized_config_adom: dict[str, Any], + _normalized_config_global: dict[str, Any], + rulebase_uid: str, + mgm_uid: str, +) -> Rulebase: + """ + Creates a NAT rulebase for the given access rulebase. + Similar to CheckPoint's cp_nat.py insert_parent_nat_rulebase. + """ + nat_rulebase_uid = "nat-rulebase-" + rulebase_uid + normalized_nat_rulebase = Rulebase( + uid=nat_rulebase_uid, + mgm_uid=mgm_uid, + name="NAT", + rules={}, + ) + + # Add to adom policies (avoid duplicates) + if not any(rb for rb in normalized_config_adom["policies"] if rb.uid == normalized_nat_rulebase.uid): + normalized_config_adom["policies"].append(normalized_nat_rulebase) + + return normalized_nat_rulebase + + +def insert_nat_rulebase_link( + from_rulebase_uid: str, + to_rulebase_uid: str, + gateway: dict[str, Any], +) -> None: + """ + Creates a RulebaseLink with link_type='nat' connecting access rulebase to NAT rulebase. + Similar to CheckPoint's cp_nat.py insert_rulebase_link. + """ + if not any( + link + for link in gateway["rulebase_links"] + if link.get("to_rulebase_uid") == to_rulebase_uid + and link.get("link_type") == "nat" + and link.get("from_rulebase_uid") == from_rulebase_uid + ): + gateway["rulebase_links"].append( + { + "from_rulebase_uid": from_rulebase_uid, + "to_rulebase_uid": to_rulebase_uid, + "type": "nat", + "is_initial": False, + "is_global": False, + "is_section": False, + } + ) + + +def extract_nat_config_fields(native_rule: dict[str, Any]) -> str: + """ + Extracts NAT-specific configuration fields from a native rule. + Returns a JSON string with NAT translation metadata. + """ + nat_config: dict[str, Any] = {} + + if native_rule.get("ippool") == 1: + nat_config["ippool"] = 1 + poolname = native_rule.get("poolname") + if isinstance(poolname, list) and poolname: + nat_config["poolname"] = poolname + elif isinstance(poolname, str) and poolname: + nat_config["poolname"] = [poolname] + + if "fixedport" in native_rule: + nat_config["fixedport"] = native_rule.get("fixedport") + + if "nat" in native_rule and native_rule["nat"] == 1: + nat_config["nat_type"] = "nat" + elif "nat46" in native_rule and native_rule["nat46"] == 1: + nat_config["nat_type"] = "nat46" + elif "nat64" in native_rule and native_rule["nat64"] == 1: + nat_config["nat_type"] = "nat64" + + return json.dumps(nat_config, sort_keys=True) if nat_config else "{}" + + +def get_nat_translated_source( + native_rule: dict[str, Any], + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], +) -> tuple[list[str], list[str]]: + if native_rule.get("ippool") == 1: + poolname = native_rule.get("poolname", []) + if isinstance(poolname, str): + poolname = [poolname] + translated_src_list = sorted(poolname) + translated_src_refs_list = [ + find_addr_ref( + pool, + is_v4=True, + normalized_config_adom=normalized_config_adom, + normalized_config_global=normalized_config_global, + ) + for pool in translated_src_list + ] + return translated_src_list, translated_src_refs_list + + rule_src_list, rule_src_refs_list = rule_parse_addresses( + native_rule, "src", normalized_config_adom, normalized_config_global, is_nat=True + ) + return rule_src_list, rule_src_refs_list + + +def parse_nat_rules_in_rulebase( + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], + rulebase_to_parse: dict[str, Any], + normalized_nat_rulebase: Rulebase, +): + """ + Extracts NAT rules from a rulebase and creates normalized NAT rules. + Creates two RuleNormalized objects per NAT rule (original + translated). + """ + rule_num = 0 + for native_rule in rulebase_to_parse.get("data", []): + # Check if this is a NAT rule + is_nat_rule = any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) + if not is_nat_rule: + continue + + rule_disabled = True + if "status" in native_rule and (native_rule["status"] == 1 or native_rule["status"] == "enable"): + rule_disabled = False + + # Parse addresses for original rule + rule_src_list, rule_src_refs_list = rule_parse_addresses( + native_rule, "src", normalized_config_adom, normalized_config_global, is_nat=True + ) + rule_dst_list, rule_dst_refs_list = rule_parse_addresses( + native_rule, "dst", normalized_config_adom, normalized_config_global, is_nat=True + ) + translated_src_list, translated_src_refs_list = get_nat_translated_source( + native_rule, normalized_config_adom, normalized_config_global + ) + + rule_svc_list, rule_svc_refs_list = rule_parse_service(native_rule) + + rule_src_zones = find_zones_in_normalized_config( + native_rule.get("srcintf", []), normalized_config_adom, normalized_config_global + ) + rule_dst_zones = find_zones_in_normalized_config( + native_rule.get("dstintf", []), normalized_config_adom, normalized_config_global + ) + + # Extract NAT config fields + nat_config_fields = extract_nat_config_fields(native_rule) + + rule_uid = native_rule.get("uuid") + if not rule_uid: + FWOLogger.warning("NAT rule without UUID, skipping") + continue + + # Create original rule (match phase) + rule_original_uid = f"{rule_uid}-original" + rule_translated_uid = f"{rule_uid}-translated" + + rule_original = RuleNormalized( + rule_num=rule_num, + rule_num_numeric=0, + rule_disabled=rule_disabled, + rule_src_neg=False, + rule_src=LIST_DELIMITER.join(rule_src_list), + rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), + rule_dst_neg=False, + rule_dst=LIST_DELIMITER.join(rule_dst_list), + rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_svc_neg=False, + rule_svc=LIST_DELIMITER.join(rule_svc_list), + rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_action=rule_parse_action(native_rule), + rule_track=rule_parse_tracking_info(native_rule), + rule_installon=rule_parse_installon(native_rule), + rule_time=rule_parse_time(native_rule), + rule_name=native_rule.get("name", ""), + rule_uid=rule_original_uid, + rule_custom_fields=None, + rule_implied=False, + rule_type=RuleType.NAT, + last_change_admin=None, + parent_rule_uid=None, + last_hit=rule_parse_last_hit(native_rule), + rule_comment=native_rule.get("comments"), + rule_src_zone=LIST_DELIMITER.join(rule_src_zones), + rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), + rule_head_text=None, + access_rule=False, + nat_rule=True, + xlate_rule_uid=rule_translated_uid, + ) + + # Create translated rule (translation phase) + # Keep the original destination and service; translate the source to the NAT pool. + rule_translated = RuleNormalized( + rule_num=rule_num, + rule_num_numeric=0, + rule_disabled=rule_disabled, + rule_src_neg=False, + rule_src=LIST_DELIMITER.join(translated_src_list), + rule_src_refs=LIST_DELIMITER.join(translated_src_refs_list), + rule_dst_neg=False, + rule_dst=LIST_DELIMITER.join(rule_dst_list), + rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_svc_neg=False, + rule_svc=LIST_DELIMITER.join(rule_svc_list), + rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_action=rule_parse_action(native_rule), + rule_track=rule_parse_tracking_info(native_rule), + rule_installon=rule_parse_installon(native_rule), + rule_time=rule_parse_time(native_rule), + rule_name=native_rule.get("name", ""), + rule_uid=rule_translated_uid, + rule_custom_fields=nat_config_fields, + rule_implied=False, + rule_type=RuleType.NAT, + last_change_admin=None, + parent_rule_uid=None, + last_hit=rule_parse_last_hit(native_rule), + rule_comment=native_rule.get("comments"), + rule_src_zone=LIST_DELIMITER.join(rule_src_zones), + rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), + rule_head_text=None, + access_rule=False, + nat_rule=True, + xlate_rule_uid=None, + ) + + # Add both rules to the NAT rulebase + if rule_original.rule_uid: + normalized_nat_rulebase.rules[rule_original.rule_uid] = rule_original + if rule_translated.rule_uid: + normalized_nat_rulebase.rules[rule_translated.rule_uid] = rule_translated + + rule_num += 1 + + def initialize_normalized_rulebase(rulebase_to_parse: dict[str, Any], mgm_uid: str) -> Rulebase: """ We use 'type' as uid/name since a rulebase may have a v4 and a v6 part @@ -140,13 +442,20 @@ def parse_rulebase( ): """Parses a native Fortinet rulebase into a normalized rulebase.""" for native_rule in rulebase_to_parse["data"]: - parse_single_rule(normalized_config_adom, normalized_config_global, native_rule, normalized_rulebase) + parse_single_rule( + normalized_config_adom, + normalized_config_global, + native_rule, + normalized_rulebase, + ) if not found_rulebase_in_global: add_implicit_deny_rule(normalized_config_adom, normalized_config_global, normalized_rulebase) def add_implicit_deny_rule( - normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any], rulebase: Rulebase + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], + rulebase: Rulebase, ): deny_rule = { "srcaddr": ["all"], @@ -215,6 +524,8 @@ def parse_single_rule( rulebase: Rulebase, ): """Parses a single native Fortinet rule into a normalized rule and adds it to the given rulebase.""" + is_nat_rule = any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) + # Extract basic rule information rule_disabled = True # Default to disabled if "status" in native_rule and (native_rule["status"] == 1 or native_rule["status"] == "enable"): @@ -225,10 +536,18 @@ def parse_single_rule( rule_track = rule_parse_tracking_info(native_rule) rule_src_list, rule_src_refs_list = rule_parse_addresses( - native_rule, "src", normalized_config_adom, normalized_config_global, is_nat=False + native_rule, + "src", + normalized_config_adom, + normalized_config_global, + is_nat=is_nat_rule, ) rule_dst_list, rule_dst_refs_list = rule_parse_addresses( - native_rule, "dst", normalized_config_adom, normalized_config_global, is_nat=False + native_rule, + "dst", + normalized_config_adom, + normalized_config_global, + is_nat=is_nat_rule, ) rule_svc_list, rule_svc_refs_list = rule_parse_service(native_rule) @@ -247,7 +566,7 @@ def parse_single_rule( time = rule_parse_time(native_rule) - # Create the normalized rule + # Create the normalized access rule rule_normalized = RuleNormalized( rule_num=0, rule_num_numeric=0, @@ -277,15 +596,16 @@ def parse_single_rule( rule_src_zone=LIST_DELIMITER.join(rule_src_zones), rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), rule_head_text=None, + access_rule=True, + nat_rule=False, ) + if rule_normalized.rule_uid is None: raise FwoImporterErrorInconsistenciesError("rule_normalized.rule_uid is None when parsing single rule") # Add the rule to the rulebase rulebase.rules[rule_normalized.rule_uid] = rule_normalized - # TODO: handle combined NAT, see handle_combined_nat_rule - def rule_parse_action(native_rule: dict[str, Any]) -> RuleAction: # Extract action - Fortinet uses 0 for deny/drop, 1 for accept @@ -341,14 +661,31 @@ def rule_parse_addresses( addr_ref_list: list[str] = [] if not is_nat: build_addr_list( - native_rule, target, normalized_config_adom, normalized_config_global, addr_list, addr_ref_list, is_v4=True + native_rule, + target, + normalized_config_adom, + normalized_config_global, + addr_list, + addr_ref_list, + is_v4=True, ) build_addr_list( - native_rule, target, normalized_config_adom, normalized_config_global, addr_list, addr_ref_list, is_v4=False + native_rule, + target, + normalized_config_adom, + normalized_config_global, + addr_list, + addr_ref_list, + is_v4=False, ) else: build_nat_addr_list( - native_rule, target, normalized_config_adom, normalized_config_global, addr_list, addr_ref_list + native_rule, + target, + normalized_config_adom, + normalized_config_global, + addr_list, + addr_ref_list, ) return addr_list, addr_ref_list @@ -393,7 +730,7 @@ def build_nat_addr_list( ) -> None: # so far only ip v4 expected if target == "src": - for addr in sorted(native_rule.get("orig-addr", [])): + for addr in sorted(native_rule.get("srcaddr", [])): addr_list.append(addr) addr_ref_list.append( find_addr_ref( @@ -404,7 +741,7 @@ def build_nat_addr_list( ) ) if target == "dst": - for addr in sorted(native_rule.get("dst-addr", [])): + for addr in sorted(native_rule.get("dstaddr", [])): addr_list.append(addr) addr_ref_list.append( find_addr_ref( @@ -417,10 +754,15 @@ def build_nat_addr_list( def find_addr_ref( - addr: str, is_v4: bool, normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any] + addr: str, + is_v4: bool, + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], ) -> str: for nw_obj in normalized_config_adom["network_objects"] + normalized_config_global.get("network_objects", []): - if addr == nw_obj["obj_name"] and ((is_v4 and ip_type(nw_obj) == 4) or (not is_v4 and ip_type(nw_obj) == 6)): # noqa: PLR2004 + if addr == nw_obj["obj_name"] and ( + (is_v4 and ip_type(nw_obj) == ip_v4_type) or (not is_v4 and ip_type(nw_obj) == ip_v6_type) + ): return nw_obj["obj_uid"] raise FwoImporterErrorInconsistenciesError(f"No ref found for '{addr}'.") @@ -498,7 +840,15 @@ def get_access_policy( options = ["extra info", "scope member", "get meta"] previous_rulebase = get_and_link_global_rulebase( - "header", previous_rulebase, global_pkg_name, native_config_global, sid, fm_api_url, options, limit, link_list + "header", + previous_rulebase, + global_pkg_name, + native_config_global, + sid, + fm_api_url, + options, + limit, + link_list, ) previous_rulebase = get_and_link_local_rulebase( @@ -515,7 +865,15 @@ def get_access_policy( ) previous_rulebase = get_and_link_global_rulebase( - "footer", previous_rulebase, global_pkg_name, native_config_global, sid, fm_api_url, options, limit, link_list + "footer", + previous_rulebase, + global_pkg_name, + native_config_global, + sid, + fm_api_url, + options, + limit, + link_list, ) device_config["rulebase_links"].extend(link_list) @@ -535,7 +893,8 @@ def get_and_link_global_rulebase( rulebase_type_prefix = "rules_global_" + header_or_footer if global_pkg_name != "": if not is_rulebase_already_fetched( - native_config_global["rulebases"], rulebase_type_prefix + "_v4_" + global_pkg_name + native_config_global["rulebases"], + rulebase_type_prefix + "_v4_" + global_pkg_name, ): fmgr_getter.update_config_with_fortinet_api_call( native_config_global["rulebases"], @@ -547,7 +906,8 @@ def get_and_link_global_rulebase( limit=limit, ) if not is_rulebase_already_fetched( - native_config_global["rulebases"], rulebase_type_prefix + "_v6_" + global_pkg_name + native_config_global["rulebases"], + rulebase_type_prefix + "_v6_" + global_pkg_name, ): # delete_v: hier auch options=options? fmgr_getter.update_config_with_fortinet_api_call( @@ -611,7 +971,9 @@ def get_and_link_local_rulebase( def find_packages( - adom_device_vdom_policy_package_structure: dict[str, Any], adom_name: str, mgm_details_device: dict[str, Any] + adom_device_vdom_policy_package_structure: dict[str, Any], + adom_name: str, + mgm_details_device: dict[str, Any], ) -> tuple[str, str]: for device in adom_device_vdom_policy_package_structure[adom_name]: for vdom in adom_device_vdom_policy_package_structure[adom_name][device]: @@ -670,7 +1032,11 @@ def build_link(previous_rulebase: str | None, full_pkg_name: str, is_global: boo def has_rulebase_data( - rulebases: list[dict[str, Any]], full_pkg_name: str, is_global: bool, version: str, pkg_name: str + rulebases: list[dict[str, Any]], + full_pkg_name: str, + is_global: bool, + version: str, + pkg_name: str, ) -> bool: """Adds name and uid to rulebase and removes empty global rulebases""" has_data = False @@ -732,13 +1098,103 @@ def get_nat_policy( def parse_nat_rulebase( - _nat_rulebase: list[dict[str, Any]], - _nat_type_string: str, - _normalized_config_adom: dict[str, Any], - _normalized_config_global: dict[str, Any], -) -> None: - # this function is not called until it is ready check git commit for reference - return + nat_rulebase: list[dict[str, Any]], + nat_type_string: str, + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], +) -> list[RuleNormalized]: + nat_rules: list[RuleNormalized] = [] + rule_number = 0 + for rule_number, rule_orig in enumerate(nat_rulebase): + rule_src_list, rule_src_refs_list = rule_parse_addresses( + rule_orig, "src", normalized_config_adom, normalized_config_global, is_nat=True + ) # because of is_nat = True, this will look for orig-addr + rule_dst_list, rule_dst_refs_list = rule_parse_addresses( + rule_orig, "dst", normalized_config_adom, normalized_config_global, is_nat=True + ) # because of is_nat = True, this will look for dst-addr + + rule_svc_list, rule_svc_refs_list = rule_parse_service(rule_orig) + + rule_src_zones = find_zones_in_normalized_config( + rule_orig.get("srcintf", []), + normalized_config_adom, + normalized_config_global, + ) + rule_dst_zones = find_zones_in_normalized_config( + rule_orig.get("dstintf", []), + normalized_config_adom, + normalized_config_global, + ) + + rule_normalized = RuleNormalized( + rule_num=rule_number, + rule_num_numeric=0, + rule_disabled=False, + rule_src_neg=False, + rule_src=LIST_DELIMITER.join(rule_src_list), + rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), + rule_dst_neg=False, + rule_dst=LIST_DELIMITER.join(rule_dst_list), + rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_svc_neg=False, + rule_svc=LIST_DELIMITER.join(rule_svc_list), + rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_action=RuleAction.DROP, + rule_track=RuleTrack.NONE, + rule_installon=nat_type_string, + rule_time="", # Time-based rules not commonly used in basic Fortinet configs + rule_name=rule_orig.get("name", ""), + rule_uid=rule_orig.get("uuid"), + rule_custom_fields=str({}), + rule_implied=False, + rule_type=RuleType.NAT, + last_change_admin=rule_orig.get("_last-modified-by", ""), + parent_rule_uid=None, + last_hit=rule_parse_last_hit(rule_orig), + rule_comment=rule_orig.get("comments"), + rule_src_zone=LIST_DELIMITER.join(rule_src_zones), + rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), + rule_head_text=None, + xlate_rule_uid=f"{rule_orig.get('uuid')}_translated" if rule_orig.get("uuid") else None, + nat_rule=True, + ) + + xlate_rule = RuleNormalized( + rule_num=rule_number, + rule_num_numeric=0, + rule_disabled=False, + rule_src_neg=False, + rule_src=LIST_DELIMITER.join(rule_src_list), + rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), + rule_dst_neg=False, + rule_dst="Original", + rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_svc_neg=False, + rule_svc=LIST_DELIMITER.join(rule_svc_list), + rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_action=RuleAction.DROP, + rule_track=RuleTrack.NONE, + rule_installon=nat_type_string, + rule_time="", # Time-based rules not commonly used in basic Fortinet configs + rule_name=rule_orig.get("name", ""), + rule_uid=f"{rule_orig.get('uuid')}_translated" if rule_orig.get("uuid") else None, + rule_custom_fields=str({}), + rule_implied=False, + rule_type=RuleType.NAT, + last_change_admin=rule_orig.get("_last-modified-by", ""), + parent_rule_uid=None, + last_hit=rule_parse_last_hit(rule_orig), + rule_comment=rule_orig.get("comments"), + rule_src_zone=LIST_DELIMITER.join(rule_src_zones), + rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), + rule_head_text=None, + nat_rule=True, + ) + + nat_rules.append(rule_normalized) + nat_rules.append(xlate_rule) + normalized_config_adom["rules"].extend(nat_rules) + return nat_rules def create_xlate_rule(rule: dict[str, Any]) -> dict[str, Any]: @@ -757,7 +1213,11 @@ def create_xlate_rule(rule: dict[str, Any]) -> dict[str, Any]: def handle_combined_nat_rule( - rule: dict[str, Any], rule_orig: dict[str, Any], config2import: dict[str, Any], nat_rule_number: int, dev_id: int + rule: dict[str, Any], + rule_orig: dict[str, Any], + config2import: dict[str, Any], + nat_rule_number: int, + dev_id: int, ) -> dict[str, Any] | None: # TODO: see fOS_rule for reference implementation raise NotImplementedError("handle_combined_nat_rule is not implemented yet") diff --git a/roles/importer/files/importer/test/test_fortiadom5ff.py b/roles/importer/files/importer/test/test_fortiadom5ff.py index 477714517c..64cf5d11a7 100644 --- a/roles/importer/files/importer/test/test_fortiadom5ff.py +++ b/roles/importer/files/importer/test/test_fortiadom5ff.py @@ -1,9 +1,16 @@ +import json from datetime import datetime, timezone +from typing import Any import pytest -from fw_modules.fortiadom5ff.fmgr_rule import rule_parse_last_hit +from fw_modules.fortiadom5ff.fmgr_rule import ( + extract_nat_config_fields, + parse_nat_rules_in_rulebase, + rule_parse_last_hit, +) from fw_modules.fortiadom5ff.fwcommon import to_time_object from fwo_exceptions import ImportInterruptionError +from models.rulebase import Rulebase from models.time_object import TimeObject from pytest_mock import MockerFixture @@ -152,3 +159,85 @@ def test_rule_parse_last_hit_returns_offset_aware_iso_timestamp(): parsed_time = datetime.fromisoformat(parsed) assert parsed_time.tzinfo is not None assert int(parsed_time.timestamp()) == epoch_seconds + + +def test_extract_nat_config_fields_serializes_poolname_and_fixedport(): + nat_config_fields = extract_nat_config_fields( + { + "nat": 1, + "ippool": 1, + "poolname": ["pool-a", "pool-b"], + "fixedport": 1, + } + ) + + assert json.loads(nat_config_fields) == { + "fixedport": 1, + "ippool": 1, + "nat_type": "nat", + "poolname": ["pool-a", "pool-b"], + } + + +def test_parse_nat_rules_in_rulebase_keeps_translation_metadata_on_translated_rule(): + normalized_config_adom = { + "network_objects": [ + {"obj_name": "src-net", "obj_uid": "src-net-uid", "obj_ip": "10.0.0.0/24"}, + {"obj_name": "dst-net", "obj_uid": "dst-net-uid", "obj_ip": "10.0.1.0/24"}, + {"obj_name": "pool-a", "obj_uid": "pool-a-uid", "obj_ip": "10.0.2.1/32"}, + ], + "zone_objects": [{"zone_name": "inside"}, {"zone_name": "outside"}], + "policies": [], + "rules": [], + } + normalized_config_global: dict[str, list[Any]] = { + "network_objects": [], + "zone_objects": [], + "policies": [], + "rules": [], + } + native_rulebase = { + "data": [ + { + "uuid": "nat-rule-uid", + "name": "nat-rule", + "nat": 1, + "status": 1, + "srcaddr": ["src-net"], + "dstaddr": ["dst-net"], + "service": ["ALL"], + "srcintf": ["inside"], + "dstintf": ["outside"], + "ippool": 1, + "poolname": ["pool-a"], + "fixedport": 1, + } + ] + } + normalized_nat_rulebase = Rulebase(uid="nat-rulebase-test", name="NAT", mgm_uid="mgm", rules={}) + + parse_nat_rules_in_rulebase( + normalized_config_adom, + normalized_config_global, + native_rulebase, + normalized_nat_rulebase, + ) + + assert set(normalized_nat_rulebase.rules) == {"nat-rule-uid-original", "nat-rule-uid-translated"} + + original_rule = normalized_nat_rulebase.rules["nat-rule-uid-original"] + assert original_rule.rule_custom_fields is None + + translated_rule = normalized_nat_rulebase.rules["nat-rule-uid-translated"] + assert translated_rule.rule_src == "pool-a" + assert translated_rule.rule_src_refs == "pool-a-uid" + assert translated_rule.rule_dst == "dst-net" + assert translated_rule.rule_dst_refs == "dst-net-uid" + assert translated_rule.rule_src_zone == "inside" + assert translated_rule.rule_dst_zone == "outside" + assert json.loads(translated_rule.rule_custom_fields or "{}") == { + "fixedport": 1, + "ippool": 1, + "nat_type": "nat", + "poolname": ["pool-a"], + } diff --git a/roles/lib/files/FWO.Data/NormalizedRule.cs b/roles/lib/files/FWO.Data/NormalizedRule.cs index ec2885b2d9..426a5809a2 100644 --- a/roles/lib/files/FWO.Data/NormalizedRule.cs +++ b/roles/lib/files/FWO.Data/NormalizedRule.cs @@ -89,6 +89,15 @@ public class NormalizedRule [JsonProperty("rule_head_text"), JsonPropertyName("rule_head_text")] public string? RuleHeadText { get; set; } + [JsonProperty("xlate_rule_uid"), JsonPropertyName("xlate_rule_uid")] + public string? XlateRule { get; set; } + + [JsonProperty("nat_rule"), JsonPropertyName("nat_rule")] + public bool NatRule { get; set; } + + [JsonProperty("access_rule"), JsonPropertyName("access_rule")] + public bool AccessRule { get; set; } = true; + /// /// Creates a NormalizedRule from a Rule. /// @@ -127,7 +136,10 @@ public static NormalizedRule FromRule(Rule rule) RuleComment = rule.Comment, RuleSrcZone = rule.RuleFromZones?.Length > 0 ? string.Join("|", rule.RuleFromZones.Select(z => z.Content.Name).Order()) : null, RuleDstZone = rule.RuleToZones?.Length > 0 ? string.Join("|", rule.RuleToZones.Select(z => z.Content.Name).Order()) : null, - RuleHeadText = rule.SectionHeader + RuleHeadText = rule.SectionHeader, + XlateRule = rule.TranslatedRule?.Uid ?? rule.XlateRule, + NatRule = rule.NatRule, + AccessRule = rule.AccessRule }; } } diff --git a/roles/lib/files/FWO.Data/Rule.cs b/roles/lib/files/FWO.Data/Rule.cs index d5016cb7bd..5fa82ce0d8 100644 --- a/roles/lib/files/FWO.Data/Rule.cs +++ b/roles/lib/files/FWO.Data/Rule.cs @@ -118,6 +118,9 @@ public class Rule [JsonProperty("nat_rule"), JsonPropertyName("nat_rule")] public bool NatRule { get; set; } + [JsonProperty("access_rule"), JsonPropertyName("access_rule")] + public bool AccessRule { get; set; } = true; + [JsonProperty("rulebase_id"), JsonPropertyName("rulebase_id")] public int RulebaseId { get; set; } @@ -148,9 +151,15 @@ public class Rule [JsonProperty("rule"), JsonPropertyName("rule")] public Rule? ParentRule { get; set; } + [JsonProperty("ruleByXlateRule"), JsonPropertyName("ruleByXlateRule")] + public Rule? TranslatedRule { get; set; } + [JsonProperty("rule_owners"), JsonPropertyName("rule_owners")] public RuleOwner?[] RuleOwner { get; set; } = []; + [JsonProperty("xlate_rule"), JsonPropertyName("xlate_rule")] + public string? XlateRule { get; set; } + public string ChangeID { get; set; } = ""; public string AdoITID { get; set; } = ""; @@ -210,6 +219,7 @@ public Rule(Rule rule) CustomFields = rule.CustomFields; Implied = rule.Implied; NatRule = rule.NatRule; + AccessRule = rule.AccessRule; RulebaseId = rule.RulebaseId; RuleOrderNumber = rule.RuleOrderNumber; EnforcingGateways = rule.EnforcingGateways; @@ -220,6 +230,7 @@ public Rule(Rule rule) Rulebase = rule.Rulebase; LastChangeAdmin = rule.LastChangeAdmin; ParentRule = rule.ParentRule; + TranslatedRule = rule.TranslatedRule; DisplayOrderNumberString = rule.DisplayOrderNumberString; DisplayOrderNumber = rule.DisplayOrderNumber; Certified = rule.Certified; @@ -236,6 +247,7 @@ public Rule(Rule rule) Detailed = rule.Detailed; UnusedSpecialUserObjects = rule.UnusedSpecialUserObjects; UnusedUpdatableObjects = rule.UnusedUpdatableObjects; + XlateRule = rule.XlateRule; } public bool IsDropRule() diff --git a/roles/tests-unit/files/FWO.Test/ExportTest.cs b/roles/tests-unit/files/FWO.Test/ExportTest.cs index b066b6dad8..bfdcaf6757 100644 --- a/roles/tests-unit/files/FWO.Test/ExportTest.cs +++ b/roles/tests-unit/files/FWO.Test/ExportTest.cs @@ -644,7 +644,7 @@ public void RulesGenerateJson() "\"rule_action\": \"accept\",\"rule_track\": \"none\",\"section_header\": \"\"," + "\"rule_metadatum\": {\"rule_metadata_id\": 0,\"rule_created\": null,\"created_import\": null,\"removed\": null,\"removed_import\": null,\"rule_first_hit\": null,\"rule_last_hit\": \"2022-04-19T00:00:00Z\",\"recertification\": [],\"recert_history\": [],\"rule_uid\": \"\",\"rules\": [],\"Recert\": false}," + "\"translate\": {\"rule_svc_neg\": false,\"rule_svc\": \"\",\"rule_services\": [],\"rule_src_neg\": false,\"rule_src\": \"\",\"rule_froms\": [],\"rule_dst_neg\": false,\"rule_dst\": \"\",\"rule_tos\": []}," + - "\"owner_name\": \"\",\"owner_id\": null,\"matches\": \"\",\"rule_custom_fields\": \"\",\"rule_implied\": false,\"nat_rule\": false,\"rulebase_id\": 0,\"rule_num\": 0,\"rule_enforced_on_gateways\": [],\"rule_installon\": null,\"rule_time\": null,\"rule_times\": [],\"violations\": [],\"rulebase\": {\"id\": 0,\"name\": \"\",\"uid\": \"\",\"mgm_id\": 0,\"is_global\": false,\"created\": 0,\"removed\": 0,\"rules\": []},\"uiuser\": null,\"rule\": null,\"rule_owners\": [],\"ChangeID\": \"\",\"AdoITID\": \"\",\"Compliance\": 0,\"ViolationDetails\": \"\",\"DisplayOrderNumberString\": \"1\",\"DisplayOrderNumber\": 1,\"Certified\": false,\"DeviceName\": \"\",\"RulebaseName\": \"\",\"DisregardedFroms\": [],\"DisregardedTos\": [],\"DisregardedServices\": [],\"ShowDisregarded\": false}," + + "\"owner_name\": \"\",\"owner_id\": null,\"matches\": \"\",\"rule_custom_fields\": \"\",\"rule_implied\": false,\"nat_rule\": false,\"access_rule\": true,\"rulebase_id\": 0,\"rule_num\": 0,\"rule_enforced_on_gateways\": [],\"rule_installon\": null,\"rule_time\": null,\"rule_times\": [],\"violations\": [],\"rulebase\": {\"id\": 0,\"name\": \"\",\"uid\": \"\",\"mgm_id\": 0,\"is_global\": false,\"created\": 0,\"removed\": 0,\"rules\": []},\"uiuser\": null,\"rule\": null,\"ruleByXlateRule\": null,\"rule_owners\": [],\"xlate_rule\": null,\"ChangeID\": \"\",\"AdoITID\": \"\",\"Compliance\": 0,\"ViolationDetails\": \"\",\"DisplayOrderNumberString\": \"1\",\"DisplayOrderNumber\": 1,\"Certified\": false,\"DeviceName\": \"\",\"RulebaseName\": \"\",\"DisregardedFroms\": [],\"DisregardedTos\": [],\"DisregardedServices\": [],\"ShowDisregarded\": false}," + "{\"rule_id\": 0,\"rule_uid\": \"uid2:123\",\"mgm_id\": 0,\"rule_num_numeric\": 0,\"rule_name\": \"TestRule2\",\"rule_comment\": \"comment2\",\"rule_disabled\": false," + "\"rule_services\": [{\"service\": {\"svc_id\": 2,\"svc_name\": \"TestService2\",\"svc_uid\": \"\",\"svc_port\": 6666,\"svc_port_end\": 7777,\"svc_source_port\": null,\"svc_source_port_end\": null,\"svc_code\": \"\",\"svc_timeout\": null,\"svc_typ_id\": null,\"active\": false,\"svc_create\": 0,\"svc_create_time\": {\"time\": \"0001-01-01T00:00:00\"},\"svc_last_seen\": 0,\"service_type\": {\"name\": \"\"},\"svc_comment\": \"\",\"svc_color_id\": null,\"stm_color\": null,\"ip_proto_id\": null,\"protocol_name\": {\"id\": 17,\"name\": \"UDP\"},\"svc_member_names\": \"\",\"svc_member_refs\": \"\",\"svcgrps\": [],\"svcgrp_flats\": [],\"svc_rpcnr\": null}}]," + "\"rule_svc_neg\": true,\"rule_svc\": \"\",\"rule_svc_refs\": \"\",\"rule_src_neg\": true,\"rule_src\": \"\",\"rule_src_refs\": \"\",\"rule_from_zones\": []," + @@ -657,7 +657,7 @@ public void RulesGenerateJson() "\"rule_action\": \"deny\",\"rule_track\": \"none\",\"section_header\": \"\"," + "\"rule_metadatum\": {\"rule_metadata_id\": 0,\"rule_created\": null,\"created_import\": null,\"removed\": null,\"removed_import\": null,\"rule_first_hit\": null,\"rule_last_hit\": null,\"recertification\": [],\"recert_history\": [],\"rule_uid\": \"\",\"rules\": [],\"Recert\": false}," + "\"translate\": {\"rule_svc_neg\": false,\"rule_svc\": \"\",\"rule_services\": [],\"rule_src_neg\": false,\"rule_src\": \"\",\"rule_froms\": [],\"rule_dst_neg\": false,\"rule_dst\": \"\",\"rule_tos\": []}," + - "\"owner_name\": \"\",\"owner_id\": null,\"matches\": \"\",\"rule_custom_fields\": \"\",\"rule_implied\": false,\"nat_rule\": false,\"rulebase_id\": 0,\"rule_num\": 0,\"rule_enforced_on_gateways\": [],\"rule_installon\": null,\"rule_time\": null,\"rule_times\": [],\"violations\": [],\"rulebase\": {\"id\": 0,\"name\": \"\",\"uid\": \"\",\"mgm_id\": 0,\"is_global\": false,\"created\": 0,\"removed\": 0,\"rules\": []},\"uiuser\": null,\"rule\": null,\"rule_owners\": [],\"ChangeID\": \"\",\"AdoITID\": \"\",\"Compliance\": 0,\"ViolationDetails\": \"\",\"DisplayOrderNumberString\": \"\",\"DisplayOrderNumber\": 2,\"Certified\": false,\"DeviceName\": \"\",\"RulebaseName\": \"\",\"DisregardedFroms\": [],\"DisregardedTos\": [],\"DisregardedServices\": [],\"ShowDisregarded\": false}]}]," + + "\"owner_name\": \"\",\"owner_id\": null,\"matches\": \"\",\"rule_custom_fields\": \"\",\"rule_implied\": false,\"nat_rule\": false,\"access_rule\": true,\"rulebase_id\": 0,\"rule_num\": 0,\"rule_enforced_on_gateways\": [],\"rule_installon\": null,\"rule_time\": null,\"rule_times\": [],\"violations\": [],\"rulebase\": {\"id\": 0,\"name\": \"\",\"uid\": \"\",\"mgm_id\": 0,\"is_global\": false,\"created\": 0,\"removed\": 0,\"rules\": []},\"uiuser\": null,\"rule\": null,\"ruleByXlateRule\": null,\"rule_owners\": [],\"xlate_rule\": null,\"ChangeID\": \"\",\"AdoITID\": \"\",\"Compliance\": 0,\"ViolationDetails\": \"\",\"DisplayOrderNumberString\": \"\",\"DisplayOrderNumber\": 2,\"Certified\": false,\"DeviceName\": \"\",\"RulebaseName\": \"\",\"DisregardedFroms\": [],\"DisregardedTos\": [],\"DisregardedServices\": [],\"ShowDisregarded\": false}]}]," + "\"changelog_rules\": null,\"changelog_objects\": null,\"changelog_services\": null,\"changelog_users\": null,\"import\": {\"aggregate\": {\"max\": {\"id\": null}}},\"import_controls\": [],\"RelevantImportId\": null,\"is_super_manager\": false,\"multi_device_manager_id\": null,\"management\": null,\"managementByMultiDeviceManagerId\": [],\"networkObjects\": [],\"serviceObjects\": [],\"userObjects\": [],\"zoneObjects\": []," + "\"reportNetworkObjects\": [{\"obj_id\": 1,\"obj_name\": \"TestIp1\",\"obj_ip\": \"1.2.3.4/32\",\"obj_ip_end\": \"1.2.3.4/32\",\"obj_uid\": \"\",\"zone\": null,\"active\": false,\"obj_create\": 0," + "\"obj_create_time\": {\"time\": \"0001-01-01T00:00:00\"},\"obj_last_seen\": 0,\"type\": {\"id\": 0,\"name\": \"network\"},\"obj_color\": null,\"obj_comment\": \"\",\"obj_member_names\": \"\",\"obj_member_refs\": \"\"," + diff --git a/roles/tests-unit/files/FWO.Test/RuleDataTest.cs b/roles/tests-unit/files/FWO.Test/RuleDataTest.cs index 18e49b2cae..746e0c2a60 100644 --- a/roles/tests-unit/files/FWO.Test/RuleDataTest.cs +++ b/roles/tests-unit/files/FWO.Test/RuleDataTest.cs @@ -47,5 +47,44 @@ public void RuleTimes_AreDeserialized_WhenRemovedIsNull() Assert.That(timeObject.Id, Is.EqualTo(55)); Assert.That(timeObject.Name, Is.EqualTo("Office Hours")); } + + [Test] + public void NormalizedRule_FromRule_PreservesNatAndTranslationFlags() + { + Rule rule = new() + { + NatRule = true, + AccessRule = false, + XlateRule = "1366", + TranslatedRule = new Rule { Uid = "translated-rule" }, + RuleOrderNumber = 7, + OrderNumber = 7.0, + Disabled = false, + SourceNegated = false, + Source = "any", + SourceRefs = "", + DestinationNegated = false, + Destination = "any", + DestinationRefs = "", + ServiceNegated = false, + Service = "any", + ServiceRefs = "", + Action = "accept", + Track = "none", + Implied = false, + Metadata = new RuleMetadata() + }; + + NormalizedRule normalizedRule = NormalizedRule.FromRule(rule); + + Assert.That(normalizedRule.NatRule, Is.True); + Assert.That(normalizedRule.AccessRule, Is.False); + Assert.That(normalizedRule.XlateRule, Is.EqualTo("translated-rule")); + + string serialized = JsonConvert.SerializeObject(normalizedRule); + Assert.That(serialized, Does.Contain("\"nat_rule\":true")); + Assert.That(serialized, Does.Contain("\"access_rule\":false")); + Assert.That(serialized, Does.Contain("\"xlate_rule_uid\":\"translated-rule\"")); + } } } From 53f72a3bef5fcffd5c0c07ad9b8b60661794eed4 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Tue, 26 May 2026 13:54:20 +0200 Subject: [PATCH 43/63] feat: simplify function --- .../fw_modules/fortiadom5ff/fmgr_rule.py | 105 +++++++++--------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index f3261812f4..4c5d25cc17 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -82,63 +82,68 @@ def normalize_rulebases_for_each_link_destination( # NAT links are generated during normalization and do not exist in native rulebases. continue - if rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "": - rulebase_to_parse = find_rulebase_to_parse(native_config["rulebases"], rulebase_link["to_rulebase_uid"]) - # search in global rulebase - found_rulebase_in_global = False - if rulebase_to_parse == {} and not is_global_loop_iteration and native_config_global != {}: - rulebase_to_parse = find_rulebase_to_parse( - native_config_global["rulebases"], rulebase_link["to_rulebase_uid"] - ) - found_rulebase_in_global = True - if rulebase_to_parse == {}: - FWOLogger.warning("found to_rulebase link without rulebase in nativeConfig: " + str(rulebase_link)) - continue + if not ( + rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "" + ): + normalize_nat_rulebase(rulebase_link, native_config, normalized_config_adom, normalized_config_global) + continue - normalized_rulebase = initialize_normalized_rulebase(rulebase_to_parse, mgm_uid) - parse_rulebase( - normalized_config_adom, - normalized_config_global, - rulebase_to_parse, - normalized_rulebase, - found_rulebase_in_global, + rulebase_to_parse = find_rulebase_to_parse(native_config["rulebases"], rulebase_link["to_rulebase_uid"]) + # search in global rulebase + found_rulebase_in_global = False + if rulebase_to_parse == {} and not is_global_loop_iteration and native_config_global != {}: + rulebase_to_parse = find_rulebase_to_parse( + native_config_global["rulebases"], rulebase_link["to_rulebase_uid"] ) - fetched_rulebase_uids.append(rulebase_link["to_rulebase_uid"]) + found_rulebase_in_global = True + if rulebase_to_parse == {}: + FWOLogger.warning("found to_rulebase link without rulebase in nativeConfig: " + str(rulebase_link)) + continue - if found_rulebase_in_global: - normalized_config_global["policies"].append(normalized_rulebase) - else: - normalized_config_adom["policies"].append(normalized_rulebase) + normalized_rulebase = initialize_normalized_rulebase(rulebase_to_parse, mgm_uid) + parse_rulebase( + normalized_config_adom, + normalized_config_global, + rulebase_to_parse, + normalized_rulebase, + found_rulebase_in_global, + ) + fetched_rulebase_uids.append(rulebase_link["to_rulebase_uid"]) - # Process NAT rules from the same rulebase - has_nat_rules = any( - any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) - for native_rule in rulebase_to_parse.get("data", []) - ) + if found_rulebase_in_global: + normalized_config_global["policies"].append(normalized_rulebase) + else: + normalized_config_adom["policies"].append(normalized_rulebase) - if has_nat_rules: - # Create NAT rulebase and link - normalized_nat_rulebase = insert_parent_nat_rulebase( - normalized_config_adom, - normalized_config_global, - normalized_rulebase.uid, - normalized_rulebase.mgm_uid, - ) + # Process NAT rules from the same rulebase + has_nat_rules = any( + any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) + for native_rule in rulebase_to_parse.get("data", []) + ) - # Create RulebaseLink from access rulebase to NAT rulebase - insert_nat_rulebase_link( - from_rulebase_uid=normalized_rulebase.uid, - to_rulebase_uid=normalized_nat_rulebase.uid, - gateway=gateway, - ) + if has_nat_rules: + # Create NAT rulebase and link + normalized_nat_rulebase = insert_parent_nat_rulebase( + normalized_config_adom, + normalized_config_global, + normalized_rulebase.uid, + normalized_rulebase.mgm_uid, + ) - # Parse NAT rules into the NAT rulebase - parse_nat_rules_in_rulebase( - normalized_config_adom, - normalized_config_global, - rulebase_to_parse, - normalized_nat_rulebase, - ) + # Create RulebaseLink from access rulebase to NAT rulebase + insert_nat_rulebase_link( + from_rulebase_uid=normalized_rulebase.uid, + to_rulebase_uid=normalized_nat_rulebase.uid, + gateway=gateway, + ) + + # Parse NAT rules into the NAT rulebase + parse_nat_rules_in_rulebase( + normalized_config_adom, + normalized_config_global, + rulebase_to_parse, + normalized_nat_rulebase, + ) # normalizing nat rulebases is work in progress normalize_nat_rulebase(rulebase_link, native_config, normalized_config_adom, normalized_config_global) From 9ec613b70d9de6985d62e4e50d21c8e8fb399a5a Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Tue, 26 May 2026 14:04:17 +0200 Subject: [PATCH 44/63] fix: Copilot issues --- .../importer/fw_modules/checkpointR8x/cp_nat.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 06fcf720ca..3d6bfc9ff3 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -97,15 +97,20 @@ def insert_parent_nat_rulebase( import_state: ImportState, normalized_config: dict[str, Any], ) -> Rulebase: + nat_rulebase_uid = "nat-rulebase-" + gateway["uid"] + existing_nat_rulebase = next((rb for rb in normalized_config["policies"] if rb.uid == nat_rulebase_uid), None) + + if existing_nat_rulebase is not None: + return existing_nat_rulebase + normalized_nat_rulebase = Rulebase( - uid="nat-rulebase-" + gateway["uid"], + uid=nat_rulebase_uid, mgm_uid=import_state.mgm_details.uid, name="NAT", rules={}, ) - if not any(rb for rb in normalized_config["policies"] if rb.uid == normalized_nat_rulebase.uid): - normalized_config["policies"].append(normalized_nat_rulebase) + normalized_config["policies"].append(normalized_nat_rulebase) return normalized_nat_rulebase @@ -231,7 +236,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "destination-negate": False, "service-negate": False, "install-on": nat_rule["install-on"], - "time": "", + "time": nat_rule.get("time", ""), "enabled": nat_rule["enabled"], "comments": nat_rule["comments"], "nat_rule": True, From 899baa2e8398746d46ec798faff9b49bb6009b73 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Tue, 26 May 2026 15:04:33 +0200 Subject: [PATCH 45/63] fix: made function easier --- .../fw_modules/fortiadom5ff/fmgr_rule.py | 138 ++++++++++++------ 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index 4c5d25cc17..c2eef2288b 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -77,25 +77,16 @@ def normalize_rulebases_for_each_link_destination( ): # Iterate over a snapshot because we may append NAT links while processing. for rulebase_link in list(gateway["rulebase_links"]): - link_type = rulebase_link.get("link_type", rulebase_link.get("type", "ordered")) - if link_type == "nat": - # NAT links are generated during normalization and do not exist in native rulebases. - continue - - if not ( - rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "" - ): + if _should_skip_rulebase_link(rulebase_link, fetched_rulebase_uids): normalize_nat_rulebase(rulebase_link, native_config, normalized_config_adom, normalized_config_global) continue - rulebase_to_parse = find_rulebase_to_parse(native_config["rulebases"], rulebase_link["to_rulebase_uid"]) - # search in global rulebase - found_rulebase_in_global = False - if rulebase_to_parse == {} and not is_global_loop_iteration and native_config_global != {}: - rulebase_to_parse = find_rulebase_to_parse( - native_config_global["rulebases"], rulebase_link["to_rulebase_uid"] - ) - found_rulebase_in_global = True + rulebase_to_parse, found_rulebase_in_global = _find_rulebase_to_parse_for_link( + rulebase_link, + native_config, + native_config_global, + is_global_loop_iteration, + ) if rulebase_to_parse == {}: FWOLogger.warning("found to_rulebase link without rulebase in nativeConfig: " + str(rulebase_link)) continue @@ -109,44 +100,95 @@ def normalize_rulebases_for_each_link_destination( found_rulebase_in_global, ) fetched_rulebase_uids.append(rulebase_link["to_rulebase_uid"]) + _append_normalized_rulebase( + normalized_config_adom, + normalized_config_global, + normalized_rulebase, + found_rulebase_in_global, + ) - if found_rulebase_in_global: - normalized_config_global["policies"].append(normalized_rulebase) - else: - normalized_config_adom["policies"].append(normalized_rulebase) - - # Process NAT rules from the same rulebase - has_nat_rules = any( - any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) - for native_rule in rulebase_to_parse.get("data", []) + _process_nat_rules_for_rulebase( + gateway, + normalized_config_adom, + normalized_config_global, + rulebase_to_parse, + normalized_rulebase, ) - if has_nat_rules: - # Create NAT rulebase and link - normalized_nat_rulebase = insert_parent_nat_rulebase( - normalized_config_adom, - normalized_config_global, - normalized_rulebase.uid, - normalized_rulebase.mgm_uid, - ) + # normalizing nat rulebases is work in progress + normalize_nat_rulebase(rulebase_link, native_config, normalized_config_adom, normalized_config_global) - # Create RulebaseLink from access rulebase to NAT rulebase - insert_nat_rulebase_link( - from_rulebase_uid=normalized_rulebase.uid, - to_rulebase_uid=normalized_nat_rulebase.uid, - gateway=gateway, - ) - # Parse NAT rules into the NAT rulebase - parse_nat_rules_in_rulebase( - normalized_config_adom, - normalized_config_global, - rulebase_to_parse, - normalized_nat_rulebase, - ) +def _should_skip_rulebase_link(rulebase_link: dict[str, Any], fetched_rulebase_uids: list[str]) -> bool: + link_type = rulebase_link.get("link_type", rulebase_link.get("type", "ordered")) + if link_type == "nat": + # NAT links are generated during normalization and do not exist in native rulebases. + return True + return not ( + rulebase_link["to_rulebase_uid"] not in fetched_rulebase_uids and rulebase_link["to_rulebase_uid"] != "" + ) - # normalizing nat rulebases is work in progress - normalize_nat_rulebase(rulebase_link, native_config, normalized_config_adom, normalized_config_global) + +def _find_rulebase_to_parse_for_link( + rulebase_link: dict[str, Any], + native_config: dict[str, Any], + native_config_global: dict[str, Any], + is_global_loop_iteration: bool, +) -> tuple[dict[str, Any], bool]: + rulebase_to_parse = find_rulebase_to_parse(native_config["rulebases"], rulebase_link["to_rulebase_uid"]) + found_rulebase_in_global = False + if rulebase_to_parse == {} and not is_global_loop_iteration and native_config_global != {}: + rulebase_to_parse = find_rulebase_to_parse(native_config_global["rulebases"], rulebase_link["to_rulebase_uid"]) + found_rulebase_in_global = True + return rulebase_to_parse, found_rulebase_in_global + + +def _append_normalized_rulebase( + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], + normalized_rulebase: Rulebase, + found_rulebase_in_global: bool, +) -> None: + if found_rulebase_in_global: + normalized_config_global["policies"].append(normalized_rulebase) + else: + normalized_config_adom["policies"].append(normalized_rulebase) + + +def _process_nat_rules_for_rulebase( + gateway: dict[str, Any], + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], + rulebase_to_parse: dict[str, Any], + normalized_rulebase: Rulebase, +) -> None: + has_nat_rules = any( + any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) + for native_rule in rulebase_to_parse.get("data", []) + ) + + if not has_nat_rules: + return + + normalized_nat_rulebase = insert_parent_nat_rulebase( + normalized_config_adom, + normalized_config_global, + normalized_rulebase.uid, + normalized_rulebase.mgm_uid, + ) + + insert_nat_rulebase_link( + from_rulebase_uid=normalized_rulebase.uid, + to_rulebase_uid=normalized_nat_rulebase.uid, + gateway=gateway, + ) + + parse_nat_rules_in_rulebase( + normalized_config_adom, + normalized_config_global, + rulebase_to_parse, + normalized_nat_rulebase, + ) def normalize_nat_rulebase( From 258e1908c3ac24252d714fc359cc514359818de5 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 2 Jun 2026 08:43:05 +0200 Subject: [PATCH 46/63] refactor: linting --- .../fw_modules/fortiadom5ff/fmgr_rule.py | 75 +++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index c2eef2288b..f1dbc2a0b8 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -6,8 +6,10 @@ from fw_modules.fortiadom5ff import fmgr_getter from fw_modules.fortiadom5ff.fmgr_consts import nat_types +from fw_modules.fortiadom5ff.fmgr_network import create_network_object +from fw_modules.fortiadom5ff.fmgr_service import create_svc_object from fw_modules.fortiadom5ff.fmgr_zone import find_zones_in_normalized_config -from fwo_const import LIST_DELIMITER +from fwo_const import ANY_IP_END, ANY_IP_START, LIST_DELIMITER from fwo_exceptions import ( FwoDeviceWithoutLocalPackageError, FwoImporterErrorInconsistenciesError, @@ -388,6 +390,30 @@ def parse_nat_rules_in_rulebase( FWOLogger.warning("NAT rule without UUID, skipping") continue + # Prepare translated fields: if a translated field equals the original, + # replace it with the standard placeholder object "Original". + ensure_original_objects(normalized_config_adom, normalized_config_global) + + translated_dst_list_local = list(rule_dst_list) + translated_dst_refs_list_local = list(rule_dst_refs_list) + translated_svc_list_local = list(rule_svc_list) + translated_svc_refs_list_local = list(rule_svc_refs_list) + + # If translation did not change the source, mark it as Original + if set(translated_src_list) == set(rule_src_list): + translated_src_list = ["Original"] + translated_src_refs_list = ["Original"] + + # If translated destination equals original destination, use Original placeholder + if set(translated_dst_list_local) == set(rule_dst_list): + translated_dst_list_local = ["Original"] + translated_dst_refs_list_local = ["Original"] + + # If translated service equals original service, use Original placeholder + if set(translated_svc_list_local) == set(rule_svc_list): + translated_svc_list_local = ["Original"] + translated_svc_refs_list_local = ["Original"] + # Create original rule (match phase) rule_original_uid = f"{rule_uid}-original" rule_translated_uid = f"{rule_uid}-translated" @@ -436,11 +462,11 @@ def parse_nat_rules_in_rulebase( rule_src=LIST_DELIMITER.join(translated_src_list), rule_src_refs=LIST_DELIMITER.join(translated_src_refs_list), rule_dst_neg=False, - rule_dst=LIST_DELIMITER.join(rule_dst_list), - rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_dst=LIST_DELIMITER.join(translated_dst_list_local), + rule_dst_refs=LIST_DELIMITER.join(translated_dst_refs_list_local), rule_svc_neg=False, - rule_svc=LIST_DELIMITER.join(rule_svc_list), - rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_svc=LIST_DELIMITER.join(translated_svc_list_local), + rule_svc_refs=LIST_DELIMITER.join(translated_svc_refs_list_local), rule_action=rule_parse_action(native_rule), rule_track=rule_parse_tracking_info(native_rule), rule_installon=rule_parse_installon(native_rule), @@ -800,6 +826,45 @@ def build_nat_addr_list( ) +def ensure_original_objects(normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any]) -> None: + """ + Ensure that a standard 'Original' network and service object exist in the normalized config. + If missing, create minimal placeholder objects in the ADOM normalized config. + """ + # Ensure lists exist + normalized_config_adom.setdefault("network_objects", []) + normalized_config_adom.setdefault("service_objects", []) + + # Check network objects in ADOM and global + combined_nw = normalized_config_adom["network_objects"] + normalized_config_global.get("network_objects", []) + if not any(obj.get("obj_name") == "Original" for obj in combined_nw): + normalized_config_adom["network_objects"].append( + create_network_object( + name="Original", + obj_type="network", + ip=ANY_IP_START, + ip_end=ANY_IP_END, + uid="Original", + color="black", + comment='"original" network object created by FWO importer for NAT purposes', + zone="global", + ) + ) + + # Check service objects in ADOM and global + combined_svc = normalized_config_adom["service_objects"] + normalized_config_global.get("service_objects", []) + if not any(svc.get("svc_name") == "Original" for svc in combined_svc): + normalized_config_adom["service_objects"].append( + create_svc_object( + name="Original", + proto=0, + color="foreground", + port=None, + comment='"original" service object created by FWO importer for NAT purposes', + ) + ) + + def find_addr_ref( addr: str, is_v4: bool, From cf3ed5d47efac3e914b8d39571ab3235562f9b04 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 2 Jun 2026 08:43:50 +0200 Subject: [PATCH 47/63] fix: use long for bigint from database --- roles/lib/files/FWO.Data/Rule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/lib/files/FWO.Data/Rule.cs b/roles/lib/files/FWO.Data/Rule.cs index 9460a3eed8..06ca3f1879 100644 --- a/roles/lib/files/FWO.Data/Rule.cs +++ b/roles/lib/files/FWO.Data/Rule.cs @@ -159,7 +159,7 @@ public class Rule public RuleOwner?[] RuleOwner { get; set; } = []; [JsonProperty("xlate_rule"), JsonPropertyName("xlate_rule")] - public string? XlateRule { get; set; } + public long? XlateRule { get; set; } [JsonProperty("flow_access_id"), JsonPropertyName("flow_access_id")] public long? FlowAccessId { get; set; } From aa7e16c754b70234ddfb636637e4f3ee110db4af Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 2 Jun 2026 08:45:01 +0200 Subject: [PATCH 48/63] fix: overwrite default egde-case --- .../files/importer/fw_modules/fortiadom5ff/fmgr_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index f1dbc2a0b8..6000c30f6d 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -199,7 +199,7 @@ def normalize_nat_rulebase( normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any], ): - normalized_config_adom["nat_policies"] = [] + normalized_config_adom.setdefault("nat_policies", []) link_type = rulebase_link.get("link_type", rulebase_link.get("type", "ordered")) if link_type == "nat": return From f58da155b0d72b76d914cb3567b61f3c281c1333 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Wed, 3 Jun 2026 09:25:24 +0200 Subject: [PATCH 49/63] fix: long string --- roles/lib/files/FWO.Data/Rule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/lib/files/FWO.Data/Rule.cs b/roles/lib/files/FWO.Data/Rule.cs index 06ca3f1879..9460a3eed8 100644 --- a/roles/lib/files/FWO.Data/Rule.cs +++ b/roles/lib/files/FWO.Data/Rule.cs @@ -159,7 +159,7 @@ public class Rule public RuleOwner?[] RuleOwner { get; set; } = []; [JsonProperty("xlate_rule"), JsonPropertyName("xlate_rule")] - public long? XlateRule { get; set; } + public string? XlateRule { get; set; } [JsonProperty("flow_access_id"), JsonPropertyName("flow_access_id")] public long? FlowAccessId { get; set; } From 4767bbcf12d09bb72034e86c2b1b5877051d2861 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 10 Jun 2026 10:14:44 +0200 Subject: [PATCH 50/63] fix: making sure that fcntl is not throwing an exception --- roles/importer/files/importer/fwo_log.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/roles/importer/files/importer/fwo_log.py b/roles/importer/files/importer/fwo_log.py index f7d7eb15eb..41883835b9 100644 --- a/roles/importer/files/importer/fwo_log.py +++ b/roles/importer/files/importer/fwo_log.py @@ -1,14 +1,17 @@ import logging import os +import sys import threading import time from collections.abc import Generator from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Literal, Protocol, TextIO, TypeAlias, cast -try: +# Returns True if the OS is Linux, macOS (Darwin), or BSD-based systems +is_unix = sys.platform in ("linux", "darwin", "freebsd") +if is_unix: import fcntl as fcntl_module -except ImportError: +else: fcntl_module = None From 40028f91f6d57b85b68f66a78cd858fde823eb98 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 10 Jun 2026 10:46:22 +0200 Subject: [PATCH 51/63] fix: tests for Original --- roles/importer/files/importer/test/test_fortiadom5ff.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/importer/files/importer/test/test_fortiadom5ff.py b/roles/importer/files/importer/test/test_fortiadom5ff.py index 64cf5d11a7..51d23135be 100644 --- a/roles/importer/files/importer/test/test_fortiadom5ff.py +++ b/roles/importer/files/importer/test/test_fortiadom5ff.py @@ -231,8 +231,8 @@ def test_parse_nat_rules_in_rulebase_keeps_translation_metadata_on_translated_ru translated_rule = normalized_nat_rulebase.rules["nat-rule-uid-translated"] assert translated_rule.rule_src == "pool-a" assert translated_rule.rule_src_refs == "pool-a-uid" - assert translated_rule.rule_dst == "dst-net" - assert translated_rule.rule_dst_refs == "dst-net-uid" + assert translated_rule.rule_dst == "Original" + assert translated_rule.rule_dst_refs == "Original" assert translated_rule.rule_src_zone == "inside" assert translated_rule.rule_dst_zone == "outside" assert json.loads(translated_rule.rule_custom_fields or "{}") == { From 4bf27cbd8c4115eeca302bf74a1c078395de5892 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 10 Jun 2026 13:08:24 +0200 Subject: [PATCH 52/63] feat: ipv6 support --- .../fw_modules/fortiadom5ff/fmgr_consts.py | 1 + .../fw_modules/fortiadom5ff/fmgr_rule.py | 23 ++++--- .../files/importer/test/test_fortiadom5ff.py | 61 +++++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_consts.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_consts.py index 6cf9e6f111..956c0e3c2f 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_consts.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_consts.py @@ -4,6 +4,7 @@ "firewall/addrgrp", "firewall/addrgrp6", "firewall/ippool", + "firewall/ippool6", "firewall/vip", "system/external-resource", "firewall/wildcard-fqdn/custom", diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index 6000c30f6d..1b0ff30eeb 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -295,6 +295,12 @@ def extract_nat_config_fields(native_rule: dict[str, Any]) -> str: if native_rule.get("ippool") == 1: nat_config["ippool"] = 1 + poolname6 = native_rule.get("poolname6") + if isinstance(poolname6, list) and poolname6: + nat_config["poolname6"] = poolname6 + elif isinstance(poolname6, str) and poolname6: + nat_config["poolname6"] = [poolname6] + poolname = native_rule.get("poolname") if isinstance(poolname, list) and poolname: nat_config["poolname"] = poolname @@ -320,14 +326,15 @@ def get_nat_translated_source( normalized_config_global: dict[str, Any], ) -> tuple[list[str], list[str]]: if native_rule.get("ippool") == 1: - poolname = native_rule.get("poolname", []) + is_ipv6 = "poolname6" in native_rule and native_rule.get("poolname6") not in (None, [], "") + poolname = native_rule.get("poolname6" if is_ipv6 else "poolname", []) if isinstance(poolname, str): poolname = [poolname] translated_src_list = sorted(poolname) translated_src_refs_list = [ find_addr_ref( pool, - is_v4=True, + is_v4=not is_ipv6, normalized_config_adom=normalized_config_adom, normalized_config_global=normalized_config_global, ) @@ -801,25 +808,27 @@ def build_nat_addr_list( addr_list: list[str], addr_ref_list: list[str], ) -> None: - # so far only ip v4 expected + is_ipv6 = bool(native_rule.get("srcaddr6") or native_rule.get("dstaddr6")) if target == "src": - for addr in sorted(native_rule.get("srcaddr", [])): + source_addrs = native_rule.get("srcaddr6", []) if is_ipv6 else native_rule.get("srcaddr", []) + for addr in sorted(source_addrs): addr_list.append(addr) addr_ref_list.append( find_addr_ref( addr, - is_v4=True, + is_v4=not is_ipv6, normalized_config_adom=normalized_config_adom, normalized_config_global=normalized_config_global, ) ) if target == "dst": - for addr in sorted(native_rule.get("dstaddr", [])): + destination_addrs = native_rule.get("dstaddr6", []) if is_ipv6 else native_rule.get("dstaddr", []) + for addr in sorted(destination_addrs): addr_list.append(addr) addr_ref_list.append( find_addr_ref( addr, - is_v4=True, + is_v4=not is_ipv6, normalized_config_adom=normalized_config_adom, normalized_config_global=normalized_config_global, ) diff --git a/roles/importer/files/importer/test/test_fortiadom5ff.py b/roles/importer/files/importer/test/test_fortiadom5ff.py index 51d23135be..1e7dbfcb7e 100644 --- a/roles/importer/files/importer/test/test_fortiadom5ff.py +++ b/roles/importer/files/importer/test/test_fortiadom5ff.py @@ -241,3 +241,64 @@ def test_parse_nat_rules_in_rulebase_keeps_translation_metadata_on_translated_ru "nat_type": "nat", "poolname": ["pool-a"], } + + +def test_parse_nat_rules_in_rulebase_supports_ipv6_nat_pool_translation(): + normalized_config_adom = { + "network_objects": [ + {"obj_name": "src-net-v6", "obj_uid": "src-net-v6-uid", "obj_ip": "2001:db8::/64"}, + {"obj_name": "dst-net-v6", "obj_uid": "dst-net-v6-uid", "obj_ip": "2001:db8:1::/64"}, + {"obj_name": "pool-v6", "obj_uid": "pool-v6-uid", "obj_ip": "2001:db8:2::1/128"}, + ], + "zone_objects": [{"zone_name": "inside"}, {"zone_name": "outside"}], + "policies": [], + "rules": [], + } + normalized_config_global: dict[str, list[Any]] = { + "network_objects": [], + "zone_objects": [], + "policies": [], + "rules": [], + } + native_rulebase = { + "data": [ + { + "uuid": "nat-rule-v6-uid", + "name": "nat-rule-v6", + "nat": 1, + "status": 1, + "srcaddr6": ["src-net-v6"], + "dstaddr6": ["dst-net-v6"], + "service": ["ALL"], + "srcintf": ["inside"], + "dstintf": ["outside"], + "ippool": 1, + "poolname6": ["pool-v6"], + "fixedport": 1, + } + ] + } + normalized_nat_rulebase = Rulebase(uid="nat-rulebase-v6-test", name="NAT", mgm_uid="mgm", rules={}) + + parse_nat_rules_in_rulebase( + normalized_config_adom, + normalized_config_global, + native_rulebase, + normalized_nat_rulebase, + ) + + assert set(normalized_nat_rulebase.rules) == {"nat-rule-v6-uid-original", "nat-rule-v6-uid-translated"} + + translated_rule = normalized_nat_rulebase.rules["nat-rule-v6-uid-translated"] + assert translated_rule.rule_src == "pool-v6" + assert translated_rule.rule_src_refs == "pool-v6-uid" + assert translated_rule.rule_dst == "Original" + assert translated_rule.rule_dst_refs == "Original" + assert translated_rule.rule_src_zone == "inside" + assert translated_rule.rule_dst_zone == "outside" + assert json.loads(translated_rule.rule_custom_fields or "{}") == { + "fixedport": 1, + "ippool": 1, + "nat_type": "nat", + "poolname6": ["pool-v6"], + } From 5416d3043fc984b83a59b91bcb8035642e90fb88 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Sun, 14 Jun 2026 13:47:39 +0200 Subject: [PATCH 53/63] feat: implemented all nat types Co-authored-by: Lennart Schmidt --- .../fw_modules/fortiadom5ff/fmgr_consts.py | 2 + .../fw_modules/fortiadom5ff/fmgr_network.py | 9 + .../fw_modules/fortiadom5ff/fmgr_rule.py | 1252 +++++++++-------- 3 files changed, 678 insertions(+), 585 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_consts.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_consts.py index 956c0e3c2f..45fb305711 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_consts.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_consts.py @@ -26,3 +26,5 @@ nat_types = ["central/dnat", "central/dnat6", "firewall/central-snat-map"] user_obj_types = ["user/local", "user/group"] + +EXPECTED_NATIP_LIST_LENGTH = 2 diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_network.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_network.py index 5a37725755..4f8fe46220 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_network.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_network.py @@ -1,6 +1,7 @@ import ipaddress from typing import Any +from fw_modules.fortiadom5ff.fmgr_consts import nw_obj_types from fw_modules.fortiadom5ff.fmgr_zone import find_zones_in_normalized_config from fwo_base import sort_and_join_refs from fwo_const import ANY_IP_END, ANY_IP_START, LIST_DELIMITER, NAT_POSTFIX @@ -78,6 +79,13 @@ def exclude_object_types_in_member_ref_search(obj_type: str, current_obj_type: s return skip_member_ref_loop +def get_native_obj_type(object_type: str) -> str: + for native_obj_type in nw_obj_types: + if object_type.endswith(native_obj_type): + return native_obj_type + return "unknown" + + def normalize_network_object( obj_orig: dict[str, Any], nw_objects: list[dict[str, Any]], @@ -142,6 +150,7 @@ def normalize_network_object( obj_orig.get("associated-interface", []), normalized_config_adom, normalized_config_global ) obj.update({"obj_zone": LIST_DELIMITER.join(associated_interfaces)}) + obj.update({"obj_native_type": get_native_obj_type(current_obj_type)}) nw_objects.append(obj) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index 1b0ff30eeb..e95adffffa 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -5,7 +5,7 @@ from typing import Any from fw_modules.fortiadom5ff import fmgr_getter -from fw_modules.fortiadom5ff.fmgr_consts import nat_types +from fw_modules.fortiadom5ff.fmgr_consts import EXPECTED_NATIP_LIST_LENGTH, nat_types from fw_modules.fortiadom5ff.fmgr_network import create_network_object from fw_modules.fortiadom5ff.fmgr_service import create_svc_object from fw_modules.fortiadom5ff.fmgr_zone import find_zones_in_normalized_config @@ -17,6 +17,7 @@ from fwo_log import FWOLogger from models.rule import RuleAction, RuleNormalized, RuleTrack, RuleType from models.rulebase import Rulebase +from netaddr import IPNetwork NETWORK_OBJECT = "network_object" STRING_PKG = "/pkg/" @@ -109,7 +110,7 @@ def normalize_rulebases_for_each_link_destination( found_rulebase_in_global, ) - _process_nat_rules_for_rulebase( + new_process_nat_rules_for_rulebase( gateway, normalized_config_adom, normalized_config_global, @@ -145,6 +146,13 @@ def _find_rulebase_to_parse_for_link( return rulebase_to_parse, found_rulebase_in_global +def find_rulebase_to_parse(rulebase_list: list[dict[str, Any]], rulebase_uid: str) -> dict[str, Any]: + for rulebase in rulebase_list: + if rulebase["uid"] == rulebase_uid: + return rulebase + return {} + + def _append_normalized_rulebase( normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any], @@ -157,554 +165,207 @@ def _append_normalized_rulebase( normalized_config_adom["policies"].append(normalized_rulebase) -def _process_nat_rules_for_rulebase( - gateway: dict[str, Any], +def initialize_normalized_rulebase(rulebase_to_parse: dict[str, Any], mgm_uid: str) -> Rulebase: + """ + We use 'type' as uid/name since a rulebase may have a v4 and a v6 part + """ + rulebase_name = rulebase_to_parse["type"] + rulebase_uid = rulebase_to_parse["type"] + return Rulebase(uid=rulebase_uid, name=rulebase_name, mgm_uid=mgm_uid, rules={}) + + +def parse_rulebase( normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any], rulebase_to_parse: dict[str, Any], normalized_rulebase: Rulebase, -) -> None: - has_nat_rules = any( - any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) - for native_rule in rulebase_to_parse.get("data", []) - ) + found_rulebase_in_global: bool, +): + """Parses a native Fortinet rulebase into a normalized rulebase.""" + for native_rule in rulebase_to_parse["data"]: + parse_single_rule( + normalized_config_adom, + normalized_config_global, + native_rule, + normalized_rulebase, + ) + if not found_rulebase_in_global: + add_implicit_deny_rule(normalized_config_adom, normalized_config_global, normalized_rulebase) - if not has_nat_rules: - return - normalized_nat_rulebase = insert_parent_nat_rulebase( - normalized_config_adom, - normalized_config_global, - normalized_rulebase.uid, - normalized_rulebase.mgm_uid, - ) +def add_implicit_deny_rule( + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], + rulebase: Rulebase, +): + deny_rule = { + "srcaddr": ["all"], + "srcaddr6": ["all"], + "dstaddr": ["all"], + "dstaddr6": ["all"], + "service": ["ALL"], + "srcintf": ["any"], + "dstintf": ["any"], + } - insert_nat_rulebase_link( - from_rulebase_uid=normalized_rulebase.uid, - to_rulebase_uid=normalized_nat_rulebase.uid, - gateway=gateway, + rule_src_list, rule_src_refs_list = rule_parse_addresses( + deny_rule, "src", normalized_config_adom, normalized_config_global, is_nat=False + ) + rule_dst_list, rule_dst_refs_list = rule_parse_addresses( + deny_rule, "dst", normalized_config_adom, normalized_config_global, is_nat=False + ) + rule_svc_list, rule_svc_refs_list = rule_parse_service(deny_rule) + rule_src_zones = find_zones_in_normalized_config( + deny_rule.get("srcintf", []), normalized_config_adom, normalized_config_global + ) + rule_dst_zones = find_zones_in_normalized_config( + deny_rule.get("dstintf", []), normalized_config_adom, normalized_config_global ) - parse_nat_rules_in_rulebase( - normalized_config_adom, - normalized_config_global, - rulebase_to_parse, - normalized_nat_rulebase, + rule_normalized = RuleNormalized( + rule_num=0, + rule_num_numeric=0, + rule_disabled=False, + rule_src_neg=False, + rule_src=LIST_DELIMITER.join(rule_src_list), + rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), + rule_dst_neg=False, + rule_dst=LIST_DELIMITER.join(rule_dst_list), + rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_svc_neg=False, + rule_svc=LIST_DELIMITER.join(rule_svc_list), + rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_action=RuleAction.DROP, + rule_track=RuleTrack.NONE, # I guess this could also have different values + rule_installon=None, + rule_time=None, # Time-based rules not commonly used in basic Fortinet configs + rule_name="Implicit Deny", + rule_uid=f"{rulebase.uid}_implicit_deny", + rule_custom_fields=str({}), + rule_implied=True, + rule_type=RuleType.ACCESS, + last_change_admin=None, + parent_rule_uid=None, + last_hit=None, + rule_comment=None, + rule_src_zone=LIST_DELIMITER.join(rule_src_zones), + rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), + rule_head_text=None, ) + if rule_normalized.rule_uid is None: + raise FwoImporterErrorInconsistenciesError("rule_normalized.rule_uid is None when adding implicit deny rule") + rulebase.rules[rule_normalized.rule_uid] = rule_normalized -def normalize_nat_rulebase( - rulebase_link: dict[str, Any], - native_config: dict[str, Any], + +def parse_single_rule( normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any], + native_rule: dict[str, Any], + rulebase: Rulebase, ): - normalized_config_adom.setdefault("nat_policies", []) - link_type = rulebase_link.get("link_type", rulebase_link.get("type", "ordered")) - if link_type == "nat": - return + """Parses a single native Fortinet rule into a normalized rule and adds it to the given rulebase.""" + is_nat_rule = any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) - if not rulebase_link["is_section"]: - for nat_type in nat_types: - nat_type_string = nat_type + "_" + rulebase_link["to_rulebase_uid"] - nat_rulebase = get_native_nat_rulebase(native_config, nat_type_string) - parse_nat_rulebase( - nat_rulebase, - nat_type_string, - normalized_config_adom, - normalized_config_global, - ) + # Extract basic rule information + rule_disabled = True # Default to disabled + if "status" in native_rule and (native_rule["status"] == 1 or native_rule["status"] == "enable"): + rule_disabled = False - normalized_config_adom["nat_policies"].extend(nat_rulebase) # pyright: ignore[reportUnknownMemberType] + rule_action = rule_parse_action(native_rule) + rule_track = rule_parse_tracking_info(native_rule) -def get_native_nat_rulebase(native_config: dict[str, Any], nat_type_string: str) -> list[dict[str, Any]]: - for nat_rulebase in native_config["nat_rulebases"]: - if nat_type_string == nat_rulebase["type"]: - return nat_rulebase["data"] - FWOLogger.warning("no nat data for " + nat_type_string) - return [] + rule_src_list, rule_src_refs_list = rule_parse_addresses( + native_rule, + "src", + normalized_config_adom, + normalized_config_global, + is_nat=is_nat_rule, + ) + rule_dst_list, rule_dst_refs_list = rule_parse_addresses( + native_rule, + "dst", + normalized_config_adom, + normalized_config_global, + is_nat=is_nat_rule, + ) + rule_svc_list, rule_svc_refs_list = rule_parse_service(native_rule) -def find_rulebase_to_parse(rulebase_list: list[dict[str, Any]], rulebase_uid: str) -> dict[str, Any]: - for rulebase in rulebase_list: - if rulebase["uid"] == rulebase_uid: - return rulebase - return {} + rule_src_zones = find_zones_in_normalized_config( + native_rule.get("srcintf", []), normalized_config_adom, normalized_config_global + ) + rule_dst_zones = find_zones_in_normalized_config( + native_rule.get("dstintf", []), normalized_config_adom, normalized_config_global + ) + rule_src_neg, rule_dst_neg, rule_svc_neg = rule_parse_negation_flags(native_rule) + rule_installon = rule_parse_installon(native_rule) -def insert_parent_nat_rulebase( - normalized_config_adom: dict[str, Any], - _normalized_config_global: dict[str, Any], - rulebase_uid: str, - mgm_uid: str, -) -> Rulebase: - """ - Creates a NAT rulebase for the given access rulebase. - Similar to CheckPoint's cp_nat.py insert_parent_nat_rulebase. - """ - nat_rulebase_uid = "nat-rulebase-" + rulebase_uid - normalized_nat_rulebase = Rulebase( - uid=nat_rulebase_uid, - mgm_uid=mgm_uid, - name="NAT", - rules={}, + last_hit = rule_parse_last_hit(native_rule) + + time = rule_parse_time(native_rule) + + # Create the normalized access rule + rule_normalized = RuleNormalized( + rule_num=0, + rule_num_numeric=0, + rule_disabled=rule_disabled, + rule_src_neg=rule_src_neg, + rule_src=LIST_DELIMITER.join(rule_src_list), + rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), + rule_dst_neg=rule_dst_neg, + rule_dst=LIST_DELIMITER.join(rule_dst_list), + rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_svc_neg=rule_svc_neg, + rule_svc=LIST_DELIMITER.join(rule_svc_list), + rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_action=rule_action, + rule_track=rule_track, + rule_installon=rule_installon, + rule_time=time, + rule_name=native_rule.get("name"), + rule_uid=native_rule.get("uuid"), + rule_custom_fields=str(native_rule.get("meta fields", {})), + rule_implied=False, + rule_type=RuleType.ACCESS, + last_change_admin=None, # native_rule.get('_last-modified-by', ''), not handled yet -> leave out to prevent mismatches + parent_rule_uid=None, + last_hit=last_hit, + rule_comment=native_rule.get("comments"), + rule_src_zone=LIST_DELIMITER.join(rule_src_zones), + rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), + rule_head_text=None, + access_rule=True, + nat_rule=False, ) - # Add to adom policies (avoid duplicates) - if not any(rb for rb in normalized_config_adom["policies"] if rb.uid == normalized_nat_rulebase.uid): - normalized_config_adom["policies"].append(normalized_nat_rulebase) + if rule_normalized.rule_uid is None: + raise FwoImporterErrorInconsistenciesError("rule_normalized.rule_uid is None when parsing single rule") - return normalized_nat_rulebase + # Add the rule to the rulebase + rulebase.rules[rule_normalized.rule_uid] = rule_normalized -def insert_nat_rulebase_link( - from_rulebase_uid: str, - to_rulebase_uid: str, - gateway: dict[str, Any], -) -> None: +def rule_parse_action(native_rule: dict[str, Any]) -> RuleAction: + # Extract action - Fortinet uses 0 for deny/drop, 1 for accept + if native_rule.get("action", 0) == 0: + return RuleAction.DROP + return RuleAction.ACCEPT + + +def rule_parse_tracking_info(native_rule: dict[str, Any]) -> RuleTrack: + # TODO: Implement more detailed logging level extraction (difference between 1/2/3?) + logtraffic = native_rule.get("logtraffic", 0) + if (isinstance(logtraffic, int) and logtraffic > 0) or (isinstance(logtraffic, str) and logtraffic != "disable"): + return RuleTrack.LOG + return RuleTrack.NONE + + +def rule_parse_service(native_rule: dict[str, Any]) -> tuple[list[str], list[str]]: """ - Creates a RulebaseLink with link_type='nat' connecting access rulebase to NAT rulebase. - Similar to CheckPoint's cp_nat.py insert_rulebase_link. - """ - if not any( - link - for link in gateway["rulebase_links"] - if link.get("to_rulebase_uid") == to_rulebase_uid - and link.get("link_type") == "nat" - and link.get("from_rulebase_uid") == from_rulebase_uid - ): - gateway["rulebase_links"].append( - { - "from_rulebase_uid": from_rulebase_uid, - "to_rulebase_uid": to_rulebase_uid, - "type": "nat", - "is_initial": False, - "is_global": False, - "is_section": False, - } - ) - - -def extract_nat_config_fields(native_rule: dict[str, Any]) -> str: - """ - Extracts NAT-specific configuration fields from a native rule. - Returns a JSON string with NAT translation metadata. - """ - nat_config: dict[str, Any] = {} - - if native_rule.get("ippool") == 1: - nat_config["ippool"] = 1 - poolname6 = native_rule.get("poolname6") - if isinstance(poolname6, list) and poolname6: - nat_config["poolname6"] = poolname6 - elif isinstance(poolname6, str) and poolname6: - nat_config["poolname6"] = [poolname6] - - poolname = native_rule.get("poolname") - if isinstance(poolname, list) and poolname: - nat_config["poolname"] = poolname - elif isinstance(poolname, str) and poolname: - nat_config["poolname"] = [poolname] - - if "fixedport" in native_rule: - nat_config["fixedport"] = native_rule.get("fixedport") - - if "nat" in native_rule and native_rule["nat"] == 1: - nat_config["nat_type"] = "nat" - elif "nat46" in native_rule and native_rule["nat46"] == 1: - nat_config["nat_type"] = "nat46" - elif "nat64" in native_rule and native_rule["nat64"] == 1: - nat_config["nat_type"] = "nat64" - - return json.dumps(nat_config, sort_keys=True) if nat_config else "{}" - - -def get_nat_translated_source( - native_rule: dict[str, Any], - normalized_config_adom: dict[str, Any], - normalized_config_global: dict[str, Any], -) -> tuple[list[str], list[str]]: - if native_rule.get("ippool") == 1: - is_ipv6 = "poolname6" in native_rule and native_rule.get("poolname6") not in (None, [], "") - poolname = native_rule.get("poolname6" if is_ipv6 else "poolname", []) - if isinstance(poolname, str): - poolname = [poolname] - translated_src_list = sorted(poolname) - translated_src_refs_list = [ - find_addr_ref( - pool, - is_v4=not is_ipv6, - normalized_config_adom=normalized_config_adom, - normalized_config_global=normalized_config_global, - ) - for pool in translated_src_list - ] - return translated_src_list, translated_src_refs_list - - rule_src_list, rule_src_refs_list = rule_parse_addresses( - native_rule, "src", normalized_config_adom, normalized_config_global, is_nat=True - ) - return rule_src_list, rule_src_refs_list - - -def parse_nat_rules_in_rulebase( - normalized_config_adom: dict[str, Any], - normalized_config_global: dict[str, Any], - rulebase_to_parse: dict[str, Any], - normalized_nat_rulebase: Rulebase, -): - """ - Extracts NAT rules from a rulebase and creates normalized NAT rules. - Creates two RuleNormalized objects per NAT rule (original + translated). - """ - rule_num = 0 - for native_rule in rulebase_to_parse.get("data", []): - # Check if this is a NAT rule - is_nat_rule = any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) - if not is_nat_rule: - continue - - rule_disabled = True - if "status" in native_rule and (native_rule["status"] == 1 or native_rule["status"] == "enable"): - rule_disabled = False - - # Parse addresses for original rule - rule_src_list, rule_src_refs_list = rule_parse_addresses( - native_rule, "src", normalized_config_adom, normalized_config_global, is_nat=True - ) - rule_dst_list, rule_dst_refs_list = rule_parse_addresses( - native_rule, "dst", normalized_config_adom, normalized_config_global, is_nat=True - ) - translated_src_list, translated_src_refs_list = get_nat_translated_source( - native_rule, normalized_config_adom, normalized_config_global - ) - - rule_svc_list, rule_svc_refs_list = rule_parse_service(native_rule) - - rule_src_zones = find_zones_in_normalized_config( - native_rule.get("srcintf", []), normalized_config_adom, normalized_config_global - ) - rule_dst_zones = find_zones_in_normalized_config( - native_rule.get("dstintf", []), normalized_config_adom, normalized_config_global - ) - - # Extract NAT config fields - nat_config_fields = extract_nat_config_fields(native_rule) - - rule_uid = native_rule.get("uuid") - if not rule_uid: - FWOLogger.warning("NAT rule without UUID, skipping") - continue - - # Prepare translated fields: if a translated field equals the original, - # replace it with the standard placeholder object "Original". - ensure_original_objects(normalized_config_adom, normalized_config_global) - - translated_dst_list_local = list(rule_dst_list) - translated_dst_refs_list_local = list(rule_dst_refs_list) - translated_svc_list_local = list(rule_svc_list) - translated_svc_refs_list_local = list(rule_svc_refs_list) - - # If translation did not change the source, mark it as Original - if set(translated_src_list) == set(rule_src_list): - translated_src_list = ["Original"] - translated_src_refs_list = ["Original"] - - # If translated destination equals original destination, use Original placeholder - if set(translated_dst_list_local) == set(rule_dst_list): - translated_dst_list_local = ["Original"] - translated_dst_refs_list_local = ["Original"] - - # If translated service equals original service, use Original placeholder - if set(translated_svc_list_local) == set(rule_svc_list): - translated_svc_list_local = ["Original"] - translated_svc_refs_list_local = ["Original"] - - # Create original rule (match phase) - rule_original_uid = f"{rule_uid}-original" - rule_translated_uid = f"{rule_uid}-translated" - - rule_original = RuleNormalized( - rule_num=rule_num, - rule_num_numeric=0, - rule_disabled=rule_disabled, - rule_src_neg=False, - rule_src=LIST_DELIMITER.join(rule_src_list), - rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), - rule_dst_neg=False, - rule_dst=LIST_DELIMITER.join(rule_dst_list), - rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), - rule_svc_neg=False, - rule_svc=LIST_DELIMITER.join(rule_svc_list), - rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), - rule_action=rule_parse_action(native_rule), - rule_track=rule_parse_tracking_info(native_rule), - rule_installon=rule_parse_installon(native_rule), - rule_time=rule_parse_time(native_rule), - rule_name=native_rule.get("name", ""), - rule_uid=rule_original_uid, - rule_custom_fields=None, - rule_implied=False, - rule_type=RuleType.NAT, - last_change_admin=None, - parent_rule_uid=None, - last_hit=rule_parse_last_hit(native_rule), - rule_comment=native_rule.get("comments"), - rule_src_zone=LIST_DELIMITER.join(rule_src_zones), - rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), - rule_head_text=None, - access_rule=False, - nat_rule=True, - xlate_rule_uid=rule_translated_uid, - ) - - # Create translated rule (translation phase) - # Keep the original destination and service; translate the source to the NAT pool. - rule_translated = RuleNormalized( - rule_num=rule_num, - rule_num_numeric=0, - rule_disabled=rule_disabled, - rule_src_neg=False, - rule_src=LIST_DELIMITER.join(translated_src_list), - rule_src_refs=LIST_DELIMITER.join(translated_src_refs_list), - rule_dst_neg=False, - rule_dst=LIST_DELIMITER.join(translated_dst_list_local), - rule_dst_refs=LIST_DELIMITER.join(translated_dst_refs_list_local), - rule_svc_neg=False, - rule_svc=LIST_DELIMITER.join(translated_svc_list_local), - rule_svc_refs=LIST_DELIMITER.join(translated_svc_refs_list_local), - rule_action=rule_parse_action(native_rule), - rule_track=rule_parse_tracking_info(native_rule), - rule_installon=rule_parse_installon(native_rule), - rule_time=rule_parse_time(native_rule), - rule_name=native_rule.get("name", ""), - rule_uid=rule_translated_uid, - rule_custom_fields=nat_config_fields, - rule_implied=False, - rule_type=RuleType.NAT, - last_change_admin=None, - parent_rule_uid=None, - last_hit=rule_parse_last_hit(native_rule), - rule_comment=native_rule.get("comments"), - rule_src_zone=LIST_DELIMITER.join(rule_src_zones), - rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), - rule_head_text=None, - access_rule=False, - nat_rule=True, - xlate_rule_uid=None, - ) - - # Add both rules to the NAT rulebase - if rule_original.rule_uid: - normalized_nat_rulebase.rules[rule_original.rule_uid] = rule_original - if rule_translated.rule_uid: - normalized_nat_rulebase.rules[rule_translated.rule_uid] = rule_translated - - rule_num += 1 - - -def initialize_normalized_rulebase(rulebase_to_parse: dict[str, Any], mgm_uid: str) -> Rulebase: - """ - We use 'type' as uid/name since a rulebase may have a v4 and a v6 part - """ - rulebase_name = rulebase_to_parse["type"] - rulebase_uid = rulebase_to_parse["type"] - return Rulebase(uid=rulebase_uid, name=rulebase_name, mgm_uid=mgm_uid, rules={}) - - -def parse_rulebase( - normalized_config_adom: dict[str, Any], - normalized_config_global: dict[str, Any], - rulebase_to_parse: dict[str, Any], - normalized_rulebase: Rulebase, - found_rulebase_in_global: bool, -): - """Parses a native Fortinet rulebase into a normalized rulebase.""" - for native_rule in rulebase_to_parse["data"]: - parse_single_rule( - normalized_config_adom, - normalized_config_global, - native_rule, - normalized_rulebase, - ) - if not found_rulebase_in_global: - add_implicit_deny_rule(normalized_config_adom, normalized_config_global, normalized_rulebase) - - -def add_implicit_deny_rule( - normalized_config_adom: dict[str, Any], - normalized_config_global: dict[str, Any], - rulebase: Rulebase, -): - deny_rule = { - "srcaddr": ["all"], - "srcaddr6": ["all"], - "dstaddr": ["all"], - "dstaddr6": ["all"], - "service": ["ALL"], - "srcintf": ["any"], - "dstintf": ["any"], - } - - rule_src_list, rule_src_refs_list = rule_parse_addresses( - deny_rule, "src", normalized_config_adom, normalized_config_global, is_nat=False - ) - rule_dst_list, rule_dst_refs_list = rule_parse_addresses( - deny_rule, "dst", normalized_config_adom, normalized_config_global, is_nat=False - ) - rule_svc_list, rule_svc_refs_list = rule_parse_service(deny_rule) - rule_src_zones = find_zones_in_normalized_config( - deny_rule.get("srcintf", []), normalized_config_adom, normalized_config_global - ) - rule_dst_zones = find_zones_in_normalized_config( - deny_rule.get("dstintf", []), normalized_config_adom, normalized_config_global - ) - - rule_normalized = RuleNormalized( - rule_num=0, - rule_num_numeric=0, - rule_disabled=False, - rule_src_neg=False, - rule_src=LIST_DELIMITER.join(rule_src_list), - rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), - rule_dst_neg=False, - rule_dst=LIST_DELIMITER.join(rule_dst_list), - rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), - rule_svc_neg=False, - rule_svc=LIST_DELIMITER.join(rule_svc_list), - rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), - rule_action=RuleAction.DROP, - rule_track=RuleTrack.NONE, # I guess this could also have different values - rule_installon=None, - rule_time=None, # Time-based rules not commonly used in basic Fortinet configs - rule_name="Implicit Deny", - rule_uid=f"{rulebase.uid}_implicit_deny", - rule_custom_fields=str({}), - rule_implied=True, - rule_type=RuleType.ACCESS, - last_change_admin=None, - parent_rule_uid=None, - last_hit=None, - rule_comment=None, - rule_src_zone=LIST_DELIMITER.join(rule_src_zones), - rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), - rule_head_text=None, - ) - - if rule_normalized.rule_uid is None: - raise FwoImporterErrorInconsistenciesError("rule_normalized.rule_uid is None when adding implicit deny rule") - rulebase.rules[rule_normalized.rule_uid] = rule_normalized - - -def parse_single_rule( - normalized_config_adom: dict[str, Any], - normalized_config_global: dict[str, Any], - native_rule: dict[str, Any], - rulebase: Rulebase, -): - """Parses a single native Fortinet rule into a normalized rule and adds it to the given rulebase.""" - is_nat_rule = any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) - - # Extract basic rule information - rule_disabled = True # Default to disabled - if "status" in native_rule and (native_rule["status"] == 1 or native_rule["status"] == "enable"): - rule_disabled = False - - rule_action = rule_parse_action(native_rule) - - rule_track = rule_parse_tracking_info(native_rule) - - rule_src_list, rule_src_refs_list = rule_parse_addresses( - native_rule, - "src", - normalized_config_adom, - normalized_config_global, - is_nat=is_nat_rule, - ) - rule_dst_list, rule_dst_refs_list = rule_parse_addresses( - native_rule, - "dst", - normalized_config_adom, - normalized_config_global, - is_nat=is_nat_rule, - ) - - rule_svc_list, rule_svc_refs_list = rule_parse_service(native_rule) - - rule_src_zones = find_zones_in_normalized_config( - native_rule.get("srcintf", []), normalized_config_adom, normalized_config_global - ) - rule_dst_zones = find_zones_in_normalized_config( - native_rule.get("dstintf", []), normalized_config_adom, normalized_config_global - ) - - rule_src_neg, rule_dst_neg, rule_svc_neg = rule_parse_negation_flags(native_rule) - rule_installon = rule_parse_installon(native_rule) - - last_hit = rule_parse_last_hit(native_rule) - - time = rule_parse_time(native_rule) - - # Create the normalized access rule - rule_normalized = RuleNormalized( - rule_num=0, - rule_num_numeric=0, - rule_disabled=rule_disabled, - rule_src_neg=rule_src_neg, - rule_src=LIST_DELIMITER.join(rule_src_list), - rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), - rule_dst_neg=rule_dst_neg, - rule_dst=LIST_DELIMITER.join(rule_dst_list), - rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), - rule_svc_neg=rule_svc_neg, - rule_svc=LIST_DELIMITER.join(rule_svc_list), - rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), - rule_action=rule_action, - rule_track=rule_track, - rule_installon=rule_installon, - rule_time=time, - rule_name=native_rule.get("name"), - rule_uid=native_rule.get("uuid"), - rule_custom_fields=str(native_rule.get("meta fields", {})), - rule_implied=False, - rule_type=RuleType.ACCESS, - last_change_admin=None, # native_rule.get('_last-modified-by', ''), not handled yet -> leave out to prevent mismatches - parent_rule_uid=None, - last_hit=last_hit, - rule_comment=native_rule.get("comments"), - rule_src_zone=LIST_DELIMITER.join(rule_src_zones), - rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), - rule_head_text=None, - access_rule=True, - nat_rule=False, - ) - - if rule_normalized.rule_uid is None: - raise FwoImporterErrorInconsistenciesError("rule_normalized.rule_uid is None when parsing single rule") - - # Add the rule to the rulebase - rulebase.rules[rule_normalized.rule_uid] = rule_normalized - - -def rule_parse_action(native_rule: dict[str, Any]) -> RuleAction: - # Extract action - Fortinet uses 0 for deny/drop, 1 for accept - if native_rule.get("action", 0) == 0: - return RuleAction.DROP - return RuleAction.ACCEPT - - -def rule_parse_tracking_info(native_rule: dict[str, Any]) -> RuleTrack: - # TODO: Implement more detailed logging level extraction (difference between 1/2/3?) - logtraffic = native_rule.get("logtraffic", 0) - if (isinstance(logtraffic, int) and logtraffic > 0) or (isinstance(logtraffic, str) and logtraffic != "disable"): - return RuleTrack.LOG - return RuleTrack.NONE - - -def rule_parse_service(native_rule: dict[str, Any]) -> tuple[list[str], list[str]]: - """ - Parses services to ordered (!) name list and reference list. + Parses services to ordered (!) name list and reference list. """ rule_svc_list: list[str] = [] rule_svc_refs_list: list[str] = [] @@ -873,6 +534,20 @@ def ensure_original_objects(normalized_config_adom: dict[str, Any], normalized_c ) ) + if not any(obj.get("obj_name") == "Outgoing Interface IP" for obj in combined_nw): + normalized_config_adom["network_objects"].append( + create_network_object( + name="Outgoing Interface IP", + obj_type="network", + ip=ANY_IP_START, + ip_end=ANY_IP_END, + uid="Outgoing_Interface_IP", + color="black", + comment='"Outgoing Interface IP" network object created by FWO importer for NAT purposes', + zone="global", + ) + ) + def find_addr_ref( addr: str, @@ -1180,6 +855,40 @@ def has_rulebase_data( return has_data +def handle_combined_nat_rule( + rule: dict[str, Any], + rule_orig: dict[str, Any], + config2import: dict[str, Any], + nat_rule_number: int, + dev_id: int, +) -> dict[str, Any] | None: + # TODO: see fOS_rule for reference implementation + raise NotImplementedError("handle_combined_nat_rule is not implemented yet") + + +def add_users_to_rule(rule_orig: dict[str, Any], rule: dict[str, Any]) -> None: + if "groups" in rule_orig: + add_users(rule_orig["groups"], rule) + if "users" in rule_orig: + add_users(rule_orig["users"], rule) + + +def add_users(users: list[str], rule: dict[str, Any]) -> None: + for user in users: + rule_src_with_users = [user + "@" + src for src in rule["rule_src"].split(LIST_DELIMITER)] + + rule["rule_src"] = LIST_DELIMITER.join(rule_src_with_users) + + # here user ref is the user name itself + rule_src_refs_with_users = [user + "@" + src for src in rule["rule_src_refs"].split(LIST_DELIMITER)] + rule["rule_src_refs"] = LIST_DELIMITER.join(rule_src_refs_with_users) + + +################### +# NAT STARTS HERE # +################### + + def get_nat_policy( sid: str, fm_api_url: str, @@ -1283,91 +992,464 @@ def parse_nat_rulebase( xlate_rule = RuleNormalized( rule_num=rule_number, rule_num_numeric=0, - rule_disabled=False, + rule_disabled=False, + rule_src_neg=False, + rule_src=LIST_DELIMITER.join(rule_src_list), + rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), + rule_dst_neg=False, + rule_dst="Original", + rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_svc_neg=False, + rule_svc=LIST_DELIMITER.join(rule_svc_list), + rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_action=RuleAction.DROP, + rule_track=RuleTrack.NONE, + rule_installon=nat_type_string, + rule_time="", # Time-based rules not commonly used in basic Fortinet configs + rule_name=rule_orig.get("name", ""), + rule_uid=f"{rule_orig.get('uuid')}_translated" if rule_orig.get("uuid") else None, + rule_custom_fields=str({}), + rule_implied=False, + rule_type=RuleType.NAT, + last_change_admin=rule_orig.get("_last-modified-by", ""), + parent_rule_uid=None, + last_hit=rule_parse_last_hit(rule_orig), + rule_comment=rule_orig.get("comments"), + rule_src_zone=LIST_DELIMITER.join(rule_src_zones), + rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), + rule_head_text=None, + nat_rule=True, + ) + + nat_rules.append(rule_normalized) + nat_rules.append(xlate_rule) + normalized_config_adom["rules"].extend(nat_rules) + return nat_rules + + +def create_xlate_rule(rule: dict[str, Any]) -> dict[str, Any]: + xlate_rule = copy.deepcopy(rule) + rule["rule_type"] = "combined" + xlate_rule["rule_type"] = "xlate" + xlate_rule["rule_comment"] = None + xlate_rule["rule_disabled"] = False + xlate_rule["rule_src"] = "Original" + xlate_rule["rule_src_refs"] = "Original" + xlate_rule["rule_dst"] = "Original" + xlate_rule["rule_dst_refs"] = "Original" + xlate_rule["rule_svc"] = "Original" + xlate_rule["rule_svc_refs"] = "Original" + return xlate_rule + + +def extract_nat_objects(nwobj_list: list[str], all_nwobjects: list[dict[str, str]]) -> list[dict[str, str]]: + nat_obj_list: list[dict[str, str]] = [] + for obj in nwobj_list: + for obj2 in all_nwobjects: + if obj2["obj_name"] == obj: + if "obj_nat_ip" in obj2: + nat_obj_list.append(obj2) + break + return nat_obj_list + + +def is_nat_rule( + native_rule: dict[str, Any], + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], +) -> tuple[bool, bool]: + is_snat = any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) + + dst_addrs = native_rule.get("dstaddr", []) + native_rule.get("dstaddr6", []) + + vip_objects: list[dict[str, Any]] = [] + network_objects = normalized_config_adom.get("network_objects", []) + normalized_config_global.get( + "network_objects", [] + ) + + for nw_obj in network_objects: + if "firewall/vip" in nw_obj.get("obj_native_type", ""): + vip_objects.extend([nw_obj]) + + for addr in dst_addrs: + if any(addr == vip_obj.get("obj_name") for vip_obj in vip_objects): + return is_snat, True + + return is_snat, False + + +def parse_nat_ip( + entries: list[str], + native_rule: dict[str, Any], + normalized_config_adom: dict[str, Any], +) -> tuple[list[str], list[str]]: + """ + Example entries: ["1.2.3.4", "255.255.255.255"] + + Creates a network object for + """ + if len(entries) != EXPECTED_NATIP_LIST_LENGTH: + FWOLogger.warning(f"Unexpected number of entries for NAT IP parsing: {len(entries)}. Expected 2.") + return [], [] + + parsed_ip = str(IPNetwork(f"{entries[0]}/{entries[1]}")) + uid = f"{native_rule.get('uuid', 'Translated_IP')}_Translated_IP" + normalized_config_adom["network_objects"].append( + create_network_object( + name=native_rule.get("name", "Translated_IP"), + obj_type="network", + ip=parsed_ip, + ip_end=parsed_ip, + uid=uid, + color="black", + comment="Translated IP network object created by FWO importer for NAT purposes", + zone="global", + ) + ) + + return [parsed_ip], [uid] + + +def parse_nat_rules_in_rulebase( + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], + rulebase_to_parse: dict[str, Any], + normalized_nat_rulebase: Rulebase, +): + """ + Extracts NAT rules from a rulebase and creates normalized NAT rules. + Creates two RuleNormalized objects per NAT rule (original + translated). + """ + rule_num = 0 + for native_rule in rulebase_to_parse.get("data", []): + # Check if this is a NAT rule + is_snat, is_dnat = is_nat_rule(native_rule, normalized_config_adom, normalized_config_global) + + if not is_snat and not is_dnat: + continue + + rule_disabled = True + if "status" in native_rule and (native_rule["status"] == 1 or native_rule["status"] == "enable"): + rule_disabled = False + + # Parse addresses for original rule + rule_src_list, rule_src_refs_list = rule_parse_addresses( + native_rule, "src", normalized_config_adom, normalized_config_global, is_nat=True + ) + rule_dst_list, rule_dst_refs_list = rule_parse_addresses( + native_rule, "dst", normalized_config_adom, normalized_config_global, is_nat=True + ) + translated_src_list, translated_src_refs_list = get_nat_translated_source( + native_rule, normalized_config_adom, normalized_config_global + ) + + rule_svc_list, rule_svc_refs_list = rule_parse_service(native_rule) + + rule_src_zones = find_zones_in_normalized_config( + native_rule.get("srcintf", []), normalized_config_adom, normalized_config_global + ) + rule_dst_zones = find_zones_in_normalized_config( + native_rule.get("dstintf", []), normalized_config_adom, normalized_config_global + ) + + # Extract NAT config fields + nat_config_fields = extract_nat_config_fields(native_rule) + + rule_uid = native_rule.get("uuid") + if not rule_uid: + FWOLogger.warning("NAT rule without UUID, skipping") + continue + + # Prepare translated fields: if a translated field equals the original, + # replace it with the standard placeholder object "Original". + ensure_original_objects(normalized_config_adom, normalized_config_global) + + translated_dst_list_local = list(rule_dst_list) + translated_dst_refs_list_local = list(rule_dst_refs_list) + translated_svc_list_local = list(rule_svc_list) + translated_svc_refs_list_local = list(rule_svc_refs_list) + + # If translation did not change the source, mark it as Original + if set(translated_src_list) == set(rule_src_list): + translated_src_list = ["Original"] + translated_src_refs_list = ["Original"] + + # If this is a SNAT rule with no IP pool, the translated source is the outgoing interface IP + if is_snat and native_rule.get("ippool") == 0: + translated_src_list = ["Outgoing Interface IP"] + translated_src_refs_list = ["Outgoing_Interface_IP"] + + # If translated destination equals original destination, use Original placeholder + if set(translated_dst_list_local) == set(rule_dst_list) and not is_dnat: + translated_dst_list_local = ["Original"] + translated_dst_refs_list_local = ["Original"] + + # If translated service equals original service, use Original placeholder + if set(translated_svc_list_local) == set(rule_svc_list): + translated_svc_list_local = ["Original"] + translated_svc_refs_list_local = ["Original"] + + if native_rule.get("rtp-nat") == 1: + translated_src_list, translated_src_refs_list = parse_nat_ip( + native_rule.get("natip", []), native_rule, normalized_config_adom + ) + + # Create original rule (match phase) + rule_original_uid = f"{rule_uid}-original" + rule_translated_uid = f"{rule_uid}-translated" + + rule_original = RuleNormalized( + rule_num=rule_num, + rule_num_numeric=0, + rule_disabled=rule_disabled, + rule_src_neg=False, + rule_src=LIST_DELIMITER.join(rule_src_list), + rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), + rule_dst_neg=False, + rule_dst=LIST_DELIMITER.join(rule_dst_list), + rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_svc_neg=False, + rule_svc=LIST_DELIMITER.join(rule_svc_list), + rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), + rule_action=rule_parse_action(native_rule), + rule_track=rule_parse_tracking_info(native_rule), + rule_installon=rule_parse_installon(native_rule), + rule_time=rule_parse_time(native_rule), + rule_name=native_rule.get("name", ""), + rule_uid=rule_original_uid, + rule_custom_fields=None, + rule_implied=False, + rule_type=RuleType.NAT, + last_change_admin=None, + parent_rule_uid=None, + last_hit=rule_parse_last_hit(native_rule), + rule_comment=native_rule.get("comments"), + rule_src_zone=LIST_DELIMITER.join(rule_src_zones), + rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), + rule_head_text=None, + access_rule=False, + nat_rule=True, + xlate_rule_uid=rule_translated_uid, + ) + + # Create translated rule (translation phase) + # Keep the original destination and service; translate the source to the NAT pool. + rule_translated = RuleNormalized( + rule_num=rule_num, + rule_num_numeric=0, + rule_disabled=rule_disabled, rule_src_neg=False, - rule_src=LIST_DELIMITER.join(rule_src_list), - rule_src_refs=LIST_DELIMITER.join(rule_src_refs_list), + rule_src=LIST_DELIMITER.join(translated_src_list), + rule_src_refs=LIST_DELIMITER.join(translated_src_refs_list), rule_dst_neg=False, - rule_dst="Original", - rule_dst_refs=LIST_DELIMITER.join(rule_dst_refs_list), + rule_dst=LIST_DELIMITER.join(translated_dst_list_local), + rule_dst_refs=LIST_DELIMITER.join(translated_dst_refs_list_local), rule_svc_neg=False, - rule_svc=LIST_DELIMITER.join(rule_svc_list), - rule_svc_refs=LIST_DELIMITER.join(rule_svc_refs_list), - rule_action=RuleAction.DROP, - rule_track=RuleTrack.NONE, - rule_installon=nat_type_string, - rule_time="", # Time-based rules not commonly used in basic Fortinet configs - rule_name=rule_orig.get("name", ""), - rule_uid=f"{rule_orig.get('uuid')}_translated" if rule_orig.get("uuid") else None, - rule_custom_fields=str({}), + rule_svc=LIST_DELIMITER.join(translated_svc_list_local), + rule_svc_refs=LIST_DELIMITER.join(translated_svc_refs_list_local), + rule_action=rule_parse_action(native_rule), + rule_track=rule_parse_tracking_info(native_rule), + rule_installon=rule_parse_installon(native_rule), + rule_time=rule_parse_time(native_rule), + rule_name=native_rule.get("name", ""), + rule_uid=rule_translated_uid, + rule_custom_fields=nat_config_fields, rule_implied=False, rule_type=RuleType.NAT, - last_change_admin=rule_orig.get("_last-modified-by", ""), + last_change_admin=None, parent_rule_uid=None, - last_hit=rule_parse_last_hit(rule_orig), - rule_comment=rule_orig.get("comments"), + last_hit=rule_parse_last_hit(native_rule), + rule_comment=native_rule.get("comments"), rule_src_zone=LIST_DELIMITER.join(rule_src_zones), rule_dst_zone=LIST_DELIMITER.join(rule_dst_zones), rule_head_text=None, + access_rule=False, nat_rule=True, + xlate_rule_uid=None, ) - nat_rules.append(rule_normalized) - nat_rules.append(xlate_rule) - normalized_config_adom["rules"].extend(nat_rules) - return nat_rules + # Add both rules to the NAT rulebase + if rule_original.rule_uid: + normalized_nat_rulebase.rules[rule_original.rule_uid] = rule_original + if rule_translated.rule_uid: + normalized_nat_rulebase.rules[rule_translated.rule_uid] = rule_translated + rule_num += 1 -def create_xlate_rule(rule: dict[str, Any]) -> dict[str, Any]: - xlate_rule = copy.deepcopy(rule) - rule["rule_type"] = "combined" - xlate_rule["rule_type"] = "xlate" - xlate_rule["rule_comment"] = None - xlate_rule["rule_disabled"] = False - xlate_rule["rule_src"] = "Original" - xlate_rule["rule_src_refs"] = "Original" - xlate_rule["rule_dst"] = "Original" - xlate_rule["rule_dst_refs"] = "Original" - xlate_rule["rule_svc"] = "Original" - xlate_rule["rule_svc_refs"] = "Original" - return xlate_rule +def extract_nat_config_fields(native_rule: dict[str, Any]) -> str: + """ + Extracts NAT-specific configuration fields from a native rule. + Returns a JSON string with NAT translation metadata. + """ + nat_config: dict[str, Any] = {} -def handle_combined_nat_rule( - rule: dict[str, Any], - rule_orig: dict[str, Any], - config2import: dict[str, Any], - nat_rule_number: int, - dev_id: int, -) -> dict[str, Any] | None: - # TODO: see fOS_rule for reference implementation - raise NotImplementedError("handle_combined_nat_rule is not implemented yet") + if native_rule.get("ippool") == 1: + nat_config["ippool"] = 1 + poolname6 = native_rule.get("poolname6") + if isinstance(poolname6, list) and poolname6: + nat_config["poolname6"] = poolname6 + elif isinstance(poolname6, str) and poolname6: + nat_config["poolname6"] = [poolname6] + poolname = native_rule.get("poolname") + if isinstance(poolname, list) and poolname: + nat_config["poolname"] = poolname + elif isinstance(poolname, str) and poolname: + nat_config["poolname"] = [poolname] -def extract_nat_objects(nwobj_list: list[str], all_nwobjects: list[dict[str, str]]) -> list[dict[str, str]]: - nat_obj_list: list[dict[str, str]] = [] - for obj in nwobj_list: - for obj2 in all_nwobjects: - if obj2["obj_name"] == obj: - if "obj_nat_ip" in obj2: - nat_obj_list.append(obj2) - break - return nat_obj_list + if "fixedport" in native_rule: + nat_config["fixedport"] = native_rule.get("fixedport") + if "nat" in native_rule and native_rule["nat"] == 1: + nat_config["nat_type"] = "nat" + elif "nat46" in native_rule and native_rule["nat46"] == 1: + nat_config["nat_type"] = "nat46" + elif "nat64" in native_rule and native_rule["nat64"] == 1: + nat_config["nat_type"] = "nat64" -def add_users_to_rule(rule_orig: dict[str, Any], rule: dict[str, Any]) -> None: - if "groups" in rule_orig: - add_users(rule_orig["groups"], rule) - if "users" in rule_orig: - add_users(rule_orig["users"], rule) + return json.dumps(nat_config, sort_keys=True) if nat_config else "{}" -def add_users(users: list[str], rule: dict[str, Any]) -> None: - for user in users: - rule_src_with_users = [user + "@" + src for src in rule["rule_src"].split(LIST_DELIMITER)] +def get_nat_translated_source( + native_rule: dict[str, Any], + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], +) -> tuple[list[str], list[str]]: + if native_rule.get("ippool") == 1: + is_ipv6 = "poolname6" in native_rule and native_rule.get("poolname6") not in (None, [], "") + poolname = native_rule.get("poolname6" if is_ipv6 else "poolname", []) + if isinstance(poolname, str): + poolname = [poolname] + translated_src_list = sorted(poolname) + translated_src_refs_list = [ + find_addr_ref( + pool, + is_v4=not is_ipv6, + normalized_config_adom=normalized_config_adom, + normalized_config_global=normalized_config_global, + ) + for pool in translated_src_list + ] + return translated_src_list, translated_src_refs_list - rule["rule_src"] = LIST_DELIMITER.join(rule_src_with_users) + rule_src_list, rule_src_refs_list = rule_parse_addresses( + native_rule, "src", normalized_config_adom, normalized_config_global, is_nat=True + ) + return rule_src_list, rule_src_refs_list - # here user ref is the user name itself - rule_src_refs_with_users = [user + "@" + src for src in rule["rule_src_refs"].split(LIST_DELIMITER)] - rule["rule_src_refs"] = LIST_DELIMITER.join(rule_src_refs_with_users) + +def new_process_nat_rules_for_rulebase( + gateway: dict[str, Any], + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], + rulebase_to_parse: dict[str, Any], + normalized_rulebase: Rulebase, +) -> None: + has_nat_rules = any( + any(key in native_rule and native_rule[key] == 1 for key in ["nat", "nat46", "nat64"]) + for native_rule in rulebase_to_parse.get("data", []) + ) + + if not has_nat_rules: + return + + normalized_nat_rulebase = insert_parent_nat_rulebase( + normalized_config_adom, + normalized_config_global, + normalized_rulebase.uid, + normalized_rulebase.mgm_uid, + ) + + insert_nat_rulebase_link( + from_rulebase_uid=normalized_rulebase.uid, + to_rulebase_uid=normalized_nat_rulebase.uid, + gateway=gateway, + ) + + parse_nat_rules_in_rulebase( + normalized_config_adom, + normalized_config_global, + rulebase_to_parse, + normalized_nat_rulebase, + ) + + +def normalize_nat_rulebase( + rulebase_link: dict[str, Any], + native_config: dict[str, Any], + normalized_config_adom: dict[str, Any], + normalized_config_global: dict[str, Any], +): + normalized_config_adom.setdefault("nat_policies", []) + link_type = rulebase_link.get("link_type", rulebase_link.get("type", "ordered")) + if link_type == "nat": + return + + if not rulebase_link["is_section"]: + for nat_type in nat_types: + nat_type_string = nat_type + "_" + rulebase_link["to_rulebase_uid"] + nat_rulebase = get_native_nat_rulebase(native_config, nat_type_string) + parse_nat_rulebase( + nat_rulebase, + nat_type_string, + normalized_config_adom, + normalized_config_global, + ) + + normalized_config_adom["nat_policies"].extend(nat_rulebase) # pyright: ignore[reportUnknownMemberType] + + +def get_native_nat_rulebase(native_config: dict[str, Any], nat_type_string: str) -> list[dict[str, Any]]: + for nat_rulebase in native_config["nat_rulebases"]: + if nat_type_string == nat_rulebase["type"]: + return nat_rulebase["data"] + FWOLogger.warning("no nat data for " + nat_type_string) + return [] + + +def insert_parent_nat_rulebase( + normalized_config_adom: dict[str, Any], + _normalized_config_global: dict[str, Any], + rulebase_uid: str, + mgm_uid: str, +) -> Rulebase: + # Creates a NAT rulebase for the given access rulebase. + nat_rulebase_uid = "nat-rulebase-" + rulebase_uid + normalized_nat_rulebase = Rulebase( + uid=nat_rulebase_uid, + mgm_uid=mgm_uid, + name="NAT", + rules={}, + ) + + # Add to adom policies (avoid duplicates) + if not any(rb for rb in normalized_config_adom["policies"] if rb.uid == normalized_nat_rulebase.uid): + normalized_config_adom["policies"].append(normalized_nat_rulebase) + + return normalized_nat_rulebase + + +def insert_nat_rulebase_link( + from_rulebase_uid: str, + to_rulebase_uid: str, + gateway: dict[str, Any], +) -> None: + # Creates a RulebaseLink with link_type='nat' connecting access rulebase to NAT rulebase. + if not any( + link + for link in gateway["rulebase_links"] + if link.get("to_rulebase_uid") == to_rulebase_uid + and link.get("link_type") == "nat" + and link.get("from_rulebase_uid") == from_rulebase_uid + ): + gateway["rulebase_links"].append( + { + "from_rulebase_uid": from_rulebase_uid, + "to_rulebase_uid": to_rulebase_uid, + "type": "nat", + "is_initial": False, + "is_global": False, + "is_section": False, + } + ) From cf3e19bdd0d928ac269ca3d999e0a8e0aa269625 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 16 Jun 2026 09:17:39 +0200 Subject: [PATCH 54/63] fix: made function easier --- .../fw_modules/fortiadom5ff/fmgr_rule.py | 95 ++++++++++++++----- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index e95adffffa..da0d2d4e83 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -1110,6 +1110,51 @@ def parse_nat_ip( return [parsed_ip], [uid] +def prepare_translated_nat_fields( + rule_src_list: list[str], + rule_dst_list: list[str], + rule_svc_list: list[str], + translated_src_list: list[str], + translated_src_refs_list: list[str], + translated_dst_list: list[str], + translated_dst_refs_list: list[str], + translated_svc_list: list[str], + translated_svc_refs_list: list[str], + native_rule: dict[str, Any], + is_snat: bool, + is_dnat: bool, +) -> tuple[list[str], list[str], list[str], list[str], list[str], list[str]]: + translated_dst_list_local = list(translated_dst_list) + translated_dst_refs_list_local = list(translated_dst_refs_list) + translated_svc_list_local = list(translated_svc_list) + translated_svc_refs_list_local = list(translated_svc_refs_list) + + if set(translated_src_list) == set(rule_src_list): + translated_src_list = ["Original"] + translated_src_refs_list = ["Original"] + + if is_snat and native_rule.get("ippool") == 0: + translated_src_list = ["Outgoing Interface IP"] + translated_src_refs_list = ["Outgoing_Interface_IP"] + + if set(translated_dst_list_local) == set(rule_dst_list) and not is_dnat: + translated_dst_list_local = ["Original"] + translated_dst_refs_list_local = ["Original"] + + if set(translated_svc_list_local) == set(rule_svc_list): + translated_svc_list_local = ["Original"] + translated_svc_refs_list_local = ["Original"] + + return ( + translated_src_list, + translated_src_refs_list, + translated_dst_list_local, + translated_dst_refs_list_local, + translated_svc_list_local, + translated_svc_refs_list_local, + ) + + def parse_nat_rules_in_rulebase( normalized_config_adom: dict[str, Any], normalized_config_global: dict[str, Any], @@ -1164,30 +1209,32 @@ def parse_nat_rules_in_rulebase( # replace it with the standard placeholder object "Original". ensure_original_objects(normalized_config_adom, normalized_config_global) - translated_dst_list_local = list(rule_dst_list) - translated_dst_refs_list_local = list(rule_dst_refs_list) - translated_svc_list_local = list(rule_svc_list) - translated_svc_refs_list_local = list(rule_svc_refs_list) - - # If translation did not change the source, mark it as Original - if set(translated_src_list) == set(rule_src_list): - translated_src_list = ["Original"] - translated_src_refs_list = ["Original"] - - # If this is a SNAT rule with no IP pool, the translated source is the outgoing interface IP - if is_snat and native_rule.get("ippool") == 0: - translated_src_list = ["Outgoing Interface IP"] - translated_src_refs_list = ["Outgoing_Interface_IP"] - - # If translated destination equals original destination, use Original placeholder - if set(translated_dst_list_local) == set(rule_dst_list) and not is_dnat: - translated_dst_list_local = ["Original"] - translated_dst_refs_list_local = ["Original"] - - # If translated service equals original service, use Original placeholder - if set(translated_svc_list_local) == set(rule_svc_list): - translated_svc_list_local = ["Original"] - translated_svc_refs_list_local = ["Original"] + translated_dst_list = list(rule_dst_list) + translated_dst_refs_list = list(rule_dst_refs_list) + translated_svc_list = list(rule_svc_list) + translated_svc_refs_list = list(rule_svc_refs_list) + + ( + translated_src_list, + translated_src_refs_list, + translated_dst_list_local, + translated_dst_refs_list_local, + translated_svc_list_local, + translated_svc_refs_list_local, + ) = prepare_translated_nat_fields( + rule_src_list, + rule_dst_list, + rule_svc_list, + translated_src_list, + translated_src_refs_list, + translated_dst_list, + translated_dst_refs_list, + translated_svc_list, + translated_svc_refs_list, + native_rule, + is_snat, + is_dnat, + ) if native_rule.get("rtp-nat") == 1: translated_src_list, translated_src_refs_list = parse_nat_ip( From ad37decb4d0d76254919a074448bfe653fafe6fe Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 16 Jun 2026 09:50:13 +0200 Subject: [PATCH 55/63] fix: more sonarqube issues --- .../fw_modules/fortiadom5ff/fmgr_rule.py | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py index da0d2d4e83..af4b2511b1 100644 --- a/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py +++ b/roles/importer/files/importer/fw_modules/fortiadom5ff/fmgr_rule.py @@ -534,16 +534,18 @@ def ensure_original_objects(normalized_config_adom: dict[str, Any], normalized_c ) ) - if not any(obj.get("obj_name") == "Outgoing Interface IP" for obj in combined_nw): + outgoing_interface_ip_name = "Outgoing Interface IP" + + if not any(obj.get("obj_name") == outgoing_interface_ip_name for obj in combined_nw): normalized_config_adom["network_objects"].append( create_network_object( - name="Outgoing Interface IP", + name=outgoing_interface_ip_name, obj_type="network", ip=ANY_IP_START, ip_end=ANY_IP_END, uid="Outgoing_Interface_IP", color="black", - comment='"Outgoing Interface IP" network object created by FWO importer for NAT purposes', + comment=f'"{outgoing_interface_ip_name}" network object created by FWO importer for NAT purposes', zone="global", ) ) @@ -1123,6 +1125,7 @@ def prepare_translated_nat_fields( native_rule: dict[str, Any], is_snat: bool, is_dnat: bool, + normalized_config_adom: dict[str, Any], ) -> tuple[list[str], list[str], list[str], list[str], list[str], list[str]]: translated_dst_list_local = list(translated_dst_list) translated_dst_refs_list_local = list(translated_dst_refs_list) @@ -1145,6 +1148,11 @@ def prepare_translated_nat_fields( translated_svc_list_local = ["Original"] translated_svc_refs_list_local = ["Original"] + if native_rule.get("rtp-nat") == 1: + translated_src_list, translated_src_refs_list = parse_nat_ip( + native_rule.get("natip", []), native_rule, normalized_config_adom + ) + return ( translated_src_list, translated_src_refs_list, @@ -1234,13 +1242,9 @@ def parse_nat_rules_in_rulebase( native_rule, is_snat, is_dnat, + normalized_config_adom, ) - if native_rule.get("rtp-nat") == 1: - translated_src_list, translated_src_refs_list = parse_nat_ip( - native_rule.get("natip", []), native_rule, normalized_config_adom - ) - # Create original rule (match phase) rule_original_uid = f"{rule_uid}-original" rule_translated_uid = f"{rule_uid}-translated" @@ -1324,6 +1328,14 @@ def parse_nat_rules_in_rulebase( rule_num += 1 +def _as_list(val: Any) -> list[Any] | None: # pyright: ignore[reportUnknownParameterType] + if isinstance(val, list) and val: + return val # pyright: ignore[reportUnknownVariableType] + if isinstance(val, str) and val: + return [val] + return None + + def extract_nat_config_fields(native_rule: dict[str, Any]) -> str: """ Extracts NAT-specific configuration fields from a native rule. @@ -1333,27 +1345,21 @@ def extract_nat_config_fields(native_rule: dict[str, Any]) -> str: if native_rule.get("ippool") == 1: nat_config["ippool"] = 1 - poolname6 = native_rule.get("poolname6") - if isinstance(poolname6, list) and poolname6: + poolname6 = _as_list(native_rule.get("poolname6")) + if poolname6 is not None: nat_config["poolname6"] = poolname6 - elif isinstance(poolname6, str) and poolname6: - nat_config["poolname6"] = [poolname6] - poolname = native_rule.get("poolname") - if isinstance(poolname, list) and poolname: + poolname = _as_list(native_rule.get("poolname")) + if poolname is not None: nat_config["poolname"] = poolname - elif isinstance(poolname, str) and poolname: - nat_config["poolname"] = [poolname] if "fixedport" in native_rule: nat_config["fixedport"] = native_rule.get("fixedport") - if "nat" in native_rule and native_rule["nat"] == 1: - nat_config["nat_type"] = "nat" - elif "nat46" in native_rule and native_rule["nat46"] == 1: - nat_config["nat_type"] = "nat46" - elif "nat64" in native_rule and native_rule["nat64"] == 1: - nat_config["nat_type"] = "nat64" + for key, name in (("nat", "nat"), ("nat46", "nat46"), ("nat64", "nat64")): + if native_rule.get(key) == 1: + nat_config["nat_type"] = name + break return json.dumps(nat_config, sort_keys=True) if nat_config else "{}" From 9cf926a58e4757a812de4e9de737a7184fc10489 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 16 Jun 2026 09:58:24 +0200 Subject: [PATCH 56/63] fix: make import safer --- roles/importer/files/importer/fwo_log.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/roles/importer/files/importer/fwo_log.py b/roles/importer/files/importer/fwo_log.py index 3cfbf157ed..190c3165a9 100644 --- a/roles/importer/files/importer/fwo_log.py +++ b/roles/importer/files/importer/fwo_log.py @@ -11,7 +11,10 @@ # Returns True if the OS is Linux, macOS (Darwin), or BSD-based systems is_unix = sys.platform in ("linux", "darwin", "freebsd") if is_unix: - import fcntl as fcntl_module + try: + import fcntl as fcntl_module + except ImportError: + fcntl_module = None else: fcntl_module = None From 3ffda47a1a34eb111f78033c6c34df8b21be7e26 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 23 Jun 2026 10:30:33 +0200 Subject: [PATCH 57/63] feat: bumped up version --- roles/database/files/upgrade/{9.0.xx.sql => 9.1.11.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename roles/database/files/upgrade/{9.0.xx.sql => 9.1.11.sql} (100%) diff --git a/roles/database/files/upgrade/9.0.xx.sql b/roles/database/files/upgrade/9.1.11.sql similarity index 100% rename from roles/database/files/upgrade/9.0.xx.sql rename to roles/database/files/upgrade/9.1.11.sql From 38ff70a5ba67627ab3076fcff44cf5cf9b47ddaa Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 23 Jun 2026 10:44:30 +0200 Subject: [PATCH 58/63] refactor: set standard time object to time --- .../files/importer/test/test_cp_nat.py | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 roles/importer/files/importer/test/test_cp_nat.py diff --git a/roles/importer/files/importer/test/test_cp_nat.py b/roles/importer/files/importer/test/test_cp_nat.py new file mode 100644 index 0000000000..4624186e2e --- /dev/null +++ b/roles/importer/files/importer/test/test_cp_nat.py @@ -0,0 +1,205 @@ +import unittest.mock + +from fw_modules.checkpointR8x.cp_nat import ( + get_initial_nat_rulebase_link, + insert_parent_nat_rulebase, + insert_rulebase_link, + parse_nat_rule_transform, +) +from models.rulebase import Rulebase + + +def _make_nat_rule(uid: str = "rule-uid-1") -> dict: + return { + "uid": uid, + "original-source": {"uid": "src-uid", "type": "host", "name": "OrigSrc"}, + "original-destination": {"uid": "dst-uid", "type": "host", "name": "OrigDst"}, + "original-service": {"uid": "svc-uid", "type": "simple", "name": "OrigSvc"}, + "translated-source": {"uid": "t-src-uid", "type": "host", "name": "TransSrc"}, + "translated-destination": {"uid": "t-dst-uid", "type": "host", "name": "TransDst"}, + "translated-service": {"uid": "t-svc-uid", "type": "simple", "name": "TransSvc"}, + "install-on": [{"uid": "gw-uid", "name": "gw"}], + "time": {"uid": "time-uid", "name": "Any"}, + "enabled": True, + "comments": "a test rule", + } + + +class TestParseNatRuleTransform: + def test_returns_tuple_of_two(self): + result = parse_nat_rule_transform(_make_nat_rule()) + assert len(result) == 2 + + def test_in_rule_maps_original_fields(self): + nat_rule = _make_nat_rule("r1") + in_rule, _ = parse_nat_rule_transform(nat_rule) + + assert in_rule["uid"] == "r1" + assert in_rule["source"] == [nat_rule["original-source"]] + assert in_rule["destination"] == [nat_rule["original-destination"]] + assert in_rule["service"] == [nat_rule["original-service"]] + assert in_rule["type"] == "nat" + assert in_rule["nat_rule"] is True + assert in_rule["access_rule"] is False + + def test_out_rule_maps_translated_fields(self): + nat_rule = _make_nat_rule("r2") + _, out_rule = parse_nat_rule_transform(nat_rule) + + assert out_rule["uid"] == "r2_translated" + assert out_rule["source"] == [nat_rule["translated-source"]] + assert out_rule["destination"] == [nat_rule["translated-destination"]] + assert out_rule["service"] == [nat_rule["translated-service"]] + assert out_rule["nat_rule"] is True + assert out_rule["access_rule"] is False + + def test_xlate_rule_uid_links_in_and_out(self): + in_rule, out_rule = parse_nat_rule_transform(_make_nat_rule("r3")) + assert in_rule["xlate_rule_uid"] == out_rule["uid"] + + def test_enabled_and_comments_propagated_to_in_rule(self): + nat_rule = _make_nat_rule() + nat_rule["enabled"] = False + nat_rule["comments"] = "disabled rule" + in_rule, _ = parse_nat_rule_transform(nat_rule) + + assert in_rule["enabled"] is False + assert in_rule["comments"] == "disabled rule" + + def test_out_rule_always_enabled(self): + nat_rule = _make_nat_rule() + nat_rule["enabled"] = False + _, out_rule = parse_nat_rule_transform(nat_rule) + + assert out_rule["enabled"] is True + + def test_in_rule_rule_number_is_zero(self): + in_rule, out_rule = parse_nat_rule_transform(_make_nat_rule()) + assert in_rule["rule-number"] == 0 + assert out_rule["rule-number"] == 0 + + def test_missing_time_field_defaults_to_empty_string(self): + nat_rule = _make_nat_rule() + del nat_rule["time"] + in_rule, _ = parse_nat_rule_transform(nat_rule) + + assert in_rule["time"] == "" + + +class TestInsertRulebaseLink: + def _make_gateway(self) -> dict: + return {"RulebaseLinks": []} + + def test_adds_new_link(self): + gateway = self._make_gateway() + insert_rulebase_link("from-rb", "to-rb", "nat", gateway) + + assert len(gateway["RulebaseLinks"]) == 1 + link = gateway["RulebaseLinks"][0] + assert link["from_rulebase_uid"] == "from-rb" + assert link["to_rulebase_uid"] == "to-rb" + assert link["link_type"] == "nat" + assert link["is_initial"] is False + assert link["is_global"] is False + assert link["is_section"] is False + + def test_does_not_add_duplicate_link(self): + gateway = self._make_gateway() + insert_rulebase_link("from-rb", "to-rb", "nat", gateway) + insert_rulebase_link("from-rb", "to-rb", "nat", gateway) + + assert len(gateway["RulebaseLinks"]) == 1 + + def test_adds_different_link_type_separately(self): + gateway = self._make_gateway() + insert_rulebase_link("from-rb", "to-rb", "nat", gateway) + insert_rulebase_link("from-rb", "to-rb", "ordered", gateway) + + assert len(gateway["RulebaseLinks"]) == 2 + + def test_adds_different_from_rulebase_separately(self): + gateway = self._make_gateway() + insert_rulebase_link("from-rb-1", "to-rb", "nat", gateway) + insert_rulebase_link("from-rb-2", "to-rb", "nat", gateway) + + assert len(gateway["RulebaseLinks"]) == 2 + + +class TestInsertParentNatRulebase: + def _make_import_state(self, mgm_uid: str = "mgm-uid-1") -> object: + mgm_details = unittest.mock.MagicMock() + mgm_details.uid = mgm_uid + import_state = unittest.mock.MagicMock() + import_state.mgm_details = mgm_details + return import_state + + def test_creates_nat_rulebase_when_missing(self): + import_state = self._make_import_state() + normalized_config = {"policies": []} + gateway = {"uid": "gw-1"} + + result = insert_parent_nat_rulebase(gateway, import_state, normalized_config) + + assert result.uid == "nat-rulebase-gw-1" + assert result.name == "NAT" + assert len(normalized_config["policies"]) == 1 + + def test_returns_existing_nat_rulebase_without_duplicate(self): + import_state = self._make_import_state() + existing = Rulebase(uid="nat-rulebase-gw-2", name="NAT", mgm_uid="mgm-uid-1") + normalized_config = {"policies": [existing]} + gateway = {"uid": "gw-2"} + + result = insert_parent_nat_rulebase(gateway, import_state, normalized_config) + + assert result is existing + assert len(normalized_config["policies"]) == 1 + + +class TestGetInitialNatRulebaseLink: + def _make_normalized_config_with_gateway(self, gateway_uid: str, rulebase_links: list) -> dict: + return { + "gateways": [ + { + "Uid": gateway_uid, + "RulebaseLinks": rulebase_links, + } + ] + } + + def test_returns_initial_ordered_link(self): + gateway = {"uid": "gw-1"} + normalized_config = self._make_normalized_config_with_gateway( + "gw-1", + [ + {"is_initial": True, "link_type": "ordered", "to_rulebase_uid": "rb-access"}, + {"is_initial": False, "link_type": "nat", "to_rulebase_uid": "rb-nat"}, + ], + ) + + result = get_initial_nat_rulebase_link(gateway, normalized_config) + + assert result is not None + assert result["to_rulebase_uid"] == "rb-access" + + def test_returns_none_when_gateway_not_found(self): + gateway = {"uid": "unknown-gw"} + normalized_config = self._make_normalized_config_with_gateway("gw-1", []) + + result = get_initial_nat_rulebase_link(gateway, normalized_config) + + assert result is None + + def test_returns_none_when_no_initial_ordered_link(self): + gateway = {"uid": "gw-1"} + normalized_config = self._make_normalized_config_with_gateway( + "gw-1", + [ + {"is_initial": False, "link_type": "ordered", "to_rulebase_uid": "rb-1"}, + {"is_initial": True, "link_type": "nat", "to_rulebase_uid": "rb-nat"}, + ], + ) + + result = get_initial_nat_rulebase_link(gateway, normalized_config) + + assert result is None From 970f8846a2e33a097ad3fdf17e386b3024e15bec Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 23 Jun 2026 10:46:16 +0200 Subject: [PATCH 59/63] refactor: time object --- .../files/importer/fw_modules/checkpointR8x/cp_nat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py index 3d6bfc9ff3..4dc3461165 100644 --- a/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py +++ b/roles/importer/files/importer/fw_modules/checkpointR8x/cp_nat.py @@ -236,7 +236,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "destination-negate": False, "service-negate": False, "install-on": nat_rule["install-on"], - "time": nat_rule.get("time", ""), + "time": nat_rule.get("time", "time"), "enabled": nat_rule["enabled"], "comments": nat_rule["comments"], "nat_rule": True, @@ -257,7 +257,7 @@ def parse_nat_rule_transform(nat_rule: dict[str, Any]) -> tuple[dict[str, Any], "destination-negate": False, "service-negate": False, "install-on": nat_rule["install-on"], - "time": "", + "time": "time", "nat_rule": True, "access_rule": False, } From 32778634f0021ce510e81360f36ddc85eb4cf6cb Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 23 Jun 2026 10:58:50 +0200 Subject: [PATCH 60/63] feat: added NAT tests --- .../files/importer/test/test_cp_nat.py | 32 ++-- roles/lib/files/FWO.Data/NormalizedRule.cs | 2 +- .../files/FWO.Test/NatRuleDisplayHtmlTest.cs | 137 ++++++++++++++++++ .../files/FWO.Test/ReportNatRulesTest.cs | 128 ++++++++++++++++ 4 files changed, 279 insertions(+), 20 deletions(-) create mode 100644 roles/tests-unit/files/FWO.Test/NatRuleDisplayHtmlTest.cs create mode 100644 roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs diff --git a/roles/importer/files/importer/test/test_cp_nat.py b/roles/importer/files/importer/test/test_cp_nat.py index 4624186e2e..31ce5c2ab5 100644 --- a/roles/importer/files/importer/test/test_cp_nat.py +++ b/roles/importer/files/importer/test/test_cp_nat.py @@ -1,4 +1,4 @@ -import unittest.mock +from typing import Any from fw_modules.checkpointR8x.cp_nat import ( get_initial_nat_rulebase_link, @@ -6,10 +6,11 @@ insert_rulebase_link, parse_nat_rule_transform, ) +from model_controllers.import_state_controller import ImportStateController from models.rulebase import Rulebase -def _make_nat_rule(uid: str = "rule-uid-1") -> dict: +def _make_nat_rule(uid: str = "rule-uid-1") -> dict[str, Any]: return { "uid": uid, "original-source": {"uid": "src-uid", "type": "host", "name": "OrigSrc"}, @@ -83,11 +84,11 @@ def test_missing_time_field_defaults_to_empty_string(self): del nat_rule["time"] in_rule, _ = parse_nat_rule_transform(nat_rule) - assert in_rule["time"] == "" + assert in_rule["time"] == "time" class TestInsertRulebaseLink: - def _make_gateway(self) -> dict: + def _make_gateway(self) -> dict[str, Any]: return {"RulebaseLinks": []} def test_adds_new_link(self): @@ -126,38 +127,31 @@ def test_adds_different_from_rulebase_separately(self): class TestInsertParentNatRulebase: - def _make_import_state(self, mgm_uid: str = "mgm-uid-1") -> object: - mgm_details = unittest.mock.MagicMock() - mgm_details.uid = mgm_uid - import_state = unittest.mock.MagicMock() - import_state.mgm_details = mgm_details - return import_state - - def test_creates_nat_rulebase_when_missing(self): - import_state = self._make_import_state() - normalized_config = {"policies": []} + def test_creates_nat_rulebase_when_missing(self, import_state_controller: ImportStateController): + normalized_config: dict[str, Any] = {"policies": []} gateway = {"uid": "gw-1"} - result = insert_parent_nat_rulebase(gateway, import_state, normalized_config) + result = insert_parent_nat_rulebase(gateway, import_state_controller.state, normalized_config) assert result.uid == "nat-rulebase-gw-1" assert result.name == "NAT" assert len(normalized_config["policies"]) == 1 - def test_returns_existing_nat_rulebase_without_duplicate(self): - import_state = self._make_import_state() + def test_returns_existing_nat_rulebase_without_duplicate(self, import_state_controller: ImportStateController): existing = Rulebase(uid="nat-rulebase-gw-2", name="NAT", mgm_uid="mgm-uid-1") normalized_config = {"policies": [existing]} gateway = {"uid": "gw-2"} - result = insert_parent_nat_rulebase(gateway, import_state, normalized_config) + result = insert_parent_nat_rulebase(gateway, import_state_controller.state, normalized_config) assert result is existing assert len(normalized_config["policies"]) == 1 class TestGetInitialNatRulebaseLink: - def _make_normalized_config_with_gateway(self, gateway_uid: str, rulebase_links: list) -> dict: + def _make_normalized_config_with_gateway( + self, gateway_uid: str, rulebase_links: list[dict[str, Any]] + ) -> dict[str, Any]: return { "gateways": [ { diff --git a/roles/lib/files/FWO.Data/NormalizedRule.cs b/roles/lib/files/FWO.Data/NormalizedRule.cs index 90e678ef81..67021c081c 100644 --- a/roles/lib/files/FWO.Data/NormalizedRule.cs +++ b/roles/lib/files/FWO.Data/NormalizedRule.cs @@ -136,7 +136,7 @@ public static NormalizedRule FromRule(Rule rule) RuleComment = rule.Comment, RuleSrcZone = rule.SourceZone, RuleDstZone = rule.DestinationZone, - RuleHeadText = rule.SectionHeader + RuleHeadText = rule.SectionHeader, XlateRule = rule.TranslatedRule?.Uid ?? rule.XlateRule, NatRule = rule.NatRule, AccessRule = rule.AccessRule diff --git a/roles/tests-unit/files/FWO.Test/NatRuleDisplayHtmlTest.cs b/roles/tests-unit/files/FWO.Test/NatRuleDisplayHtmlTest.cs new file mode 100644 index 0000000000..2b60a04063 --- /dev/null +++ b/roles/tests-unit/files/FWO.Test/NatRuleDisplayHtmlTest.cs @@ -0,0 +1,137 @@ +using FWO.Basics; +using FWO.Data; +using FWO.Report; +using FWO.Report.Filter; +using FWO.Ui.Display; +using NUnit.Framework; + +namespace FWO.Test +{ + [TestFixture] + [Parallelizable] + internal class NatRuleDisplayHtmlTest + { + private NatRuleDisplayHtml _display = null!; + + [SetUp] + public void SetUp() + { + _display = new NatRuleDisplayHtml(new SimulatedUserConfig()); + } + + private static Rule MakeNatRule(NatData natData) => + new Rule { Id = 1, MgmtId = 1, NatData = natData }; + + // ── DisplayTranslatedSource ────────────────────────────────────────── + + [Test] + public void DisplayTranslatedSource_EmptyFroms_ReturnsEmpty() + { + var rule = MakeNatRule(new NatData { TranslatedFroms = [] }); + + var result = _display.DisplayTranslatedSource(rule, OutputLocation.export); + + Assert.That(result.Trim(), Is.Empty); + } + + [Test] + public void DisplayTranslatedSource_NegatedWithEmptyFroms_ContainsNegatedText() + { + var rule = MakeNatRule(new NatData { TranslatedSourceNegated = true, TranslatedFroms = [] }); + + var result = _display.DisplayTranslatedSource(rule, OutputLocation.export); + + Assert.That(result, Does.Contain("not")); + } + + [Test] + public void DisplayTranslatedSource_NotNegated_DoesNotContainNegatedText() + { + var rule = MakeNatRule(new NatData { TranslatedSourceNegated = false, TranslatedFroms = [] }); + + var result = _display.DisplayTranslatedSource(rule, OutputLocation.export); + + Assert.That(result, Does.Not.Contain("not")); + } + + // ── DisplayTranslatedDestination ───────────────────────────────────── + + [Test] + public void DisplayTranslatedDestination_EmptyTos_ReturnsEmpty() + { + var rule = MakeNatRule(new NatData { TranslatedTos = [] }); + + var result = _display.DisplayTranslatedDestination(rule, OutputLocation.export); + + Assert.That(result.Trim(), Is.Empty); + } + + [Test] + public void DisplayTranslatedDestination_NegatedWithEmptyTos_ContainsNegatedText() + { + var rule = MakeNatRule(new NatData { TranslatedDestinationNegated = true, TranslatedTos = [] }); + + var result = _display.DisplayTranslatedDestination(rule, OutputLocation.export); + + Assert.That(result, Does.Contain("not")); + } + + [Test] + public void DisplayTranslatedDestination_NotNegated_DoesNotContainNegatedText() + { + var rule = MakeNatRule(new NatData { TranslatedDestinationNegated = false, TranslatedTos = [] }); + + var result = _display.DisplayTranslatedDestination(rule, OutputLocation.export); + + Assert.That(result, Does.Not.Contain("not")); + } + + // ── DisplayTranslatedService ───────────────────────────────────────── + + [Test] + public void DisplayTranslatedService_EmptyServices_ReturnsEmpty() + { + var rule = MakeNatRule(new NatData { TranslatedServices = [] }); + + var result = _display.DisplayTranslatedService(rule, OutputLocation.export); + + Assert.That(result.Trim(), Is.Empty); + } + + [Test] + public void DisplayTranslatedService_NegatedWithEmptyServices_ContainsNegatedText() + { + var rule = MakeNatRule(new NatData { TranslatedServiceNegated = true, TranslatedServices = [] }); + + var result = _display.DisplayTranslatedService(rule, OutputLocation.export); + + Assert.That(result, Does.Contain("not")); + } + + [Test] + public void DisplayTranslatedService_NotNegated_DoesNotContainNegatedText() + { + var rule = MakeNatRule(new NatData { TranslatedServiceNegated = false, TranslatedServices = [] }); + + var result = _display.DisplayTranslatedService(rule, OutputLocation.export); + + Assert.That(result, Does.Not.Contain("not")); + } + + [Test] + public void DisplayTranslatedService_WithOneService_ContainsServiceName() + { + var rule = MakeNatRule(new NatData + { + TranslatedServices = + [ + new ServiceWrapper { Content = new NetworkService { Name = "HTTPS", DestinationPort = 443, Protocol = new() { Id = 6, Name = "TCP" } } } + ] + }); + + var result = _display.DisplayTranslatedService(rule, OutputLocation.export); + + Assert.That(result, Does.Contain("HTTPS")); + } + } +} diff --git a/roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs b/roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs new file mode 100644 index 0000000000..bf1b5eab1f --- /dev/null +++ b/roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs @@ -0,0 +1,128 @@ +using FWO.Data; +using FWO.Report; +using FWO.Report.Filter; +using FWO.Ui.Display; +using NUnit.Framework; + +namespace FWO.Test +{ + [TestFixture] + [Parallelizable] + internal class ReportNatRulesTest + { + private NatRuleDisplayHtml _display = null!; + private ReportNatRules _report = null!; + + [SetUp] + public void SetUp() + { + var userConfig = new SimulatedUserConfig(); + _display = new NatRuleDisplayHtml(userConfig); + _report = new ReportNatRules(new DynGraphqlQuery(""), userConfig, ReportType.NatRules); + } + + [Test] + public void ExportSingleRulebaseToHtml_EmptyRuleArray_ReturnsEmptyString() + { + var result = _report.ExportSingleRulebaseToHtml([], _display, chapterNumber: 1); + + Assert.That(result.Trim(), Is.Empty); + } + + [Test] + public void ExportSingleRulebaseToHtml_NormalRule_ContainsTableRow() + { + var rules = new[] + { + new Rule + { + Id = 1, + Uid = "rule-uid-1", + Name = "TestRule", + SectionHeader = "", + NatData = new NatData() + } + }; + + var result = _report.ExportSingleRulebaseToHtml(rules, _display, chapterNumber: 1); + + Assert.That(result, Does.Contain("")); + Assert.That(result, Does.Contain("")); + } + + [Test] + public void ExportSingleRulebaseToHtml_NormalRule_DoesNotContainColspan() + { + var rules = new[] + { + new Rule + { + Id = 1, + Uid = "rule-uid-1", + SectionHeader = "", + NatData = new NatData() + } + }; + + var result = _report.ExportSingleRulebaseToHtml(rules, _display, chapterNumber: 1); + + Assert.That(result, Does.Not.Contain("colspan")); + } + + [Test] + public void ExportSingleRulebaseToHtml_SectionHeaderRule_ContainsColspan() + { + var rules = new[] + { + new Rule + { + Id = 2, + SectionHeader = "My Section", + NatData = new NatData() + } + }; + + var result = _report.ExportSingleRulebaseToHtml(rules, _display, chapterNumber: 1); + + Assert.That(result, Does.Contain("colspan")); + Assert.That(result, Does.Contain("My Section")); + } + + [Test] + public void ExportSingleRulebaseToHtml_SectionHeaderRule_DoesNotContainRegularRuleCells() + { + var rules = new[] + { + new Rule + { + Id = 3, + SectionHeader = "Section A", + NatData = new NatData() + } + }; + + var result = _report.ExportSingleRulebaseToHtml(rules, _display, chapterNumber: 1); + + // A section row gets a single merged cell, no individual for each column + Assert.That(result, Does.Not.Contain("")); + var tdCount = result.Split("").Length - 1; + Assert.That(openTrCount, Is.EqualTo(3)); + } + } +} From 36e2c40c1c9816b6e3f0ece0d1302d2d52240695 Mon Sep 17 00:00:00 2001 From: ErikPre Date: Tue, 23 Jun 2026 11:12:48 +0200 Subject: [PATCH 61/63] fix: cs tests --- roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs b/roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs index bf1b5eab1f..9ce212002a 100644 --- a/roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs +++ b/roles/tests-unit/files/FWO.Test/ReportNatRulesTest.cs @@ -1,4 +1,6 @@ +using FWO.Basics; using FWO.Data; +using FWO.Data.Report; using FWO.Report; using FWO.Report.Filter; using FWO.Ui.Display; From 6e18a4f1c60fda42eabde731b224bcfbc9d861a4 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 24 Jun 2026 16:36:14 +0200 Subject: [PATCH 62/63] feat: version bump --- inventory/group_vars/all.yml | 2 +- roles/database/files/upgrade/{9.1.11.sql => 9.1.12.sql} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename roles/database/files/upgrade/{9.1.11.sql => 9.1.12.sql} (100%) diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml index 671f31b0d0..6279cd945d 100644 --- a/inventory/group_vars/all.yml +++ b/inventory/group_vars/all.yml @@ -1,5 +1,5 @@ ### general settings -product_version: "9.1.10" +product_version: "9.1.12" ansible_user: "{{ lookup('env', 'USER') }}" ansible_become_method: sudo ansible_python_interpreter: /usr/bin/python3 diff --git a/roles/database/files/upgrade/9.1.11.sql b/roles/database/files/upgrade/9.1.12.sql similarity index 100% rename from roles/database/files/upgrade/9.1.11.sql rename to roles/database/files/upgrade/9.1.12.sql From d5a8164891f9d3e8ab2cd57c338ba86a76def593 Mon Sep 17 00:00:00 2001 From: Erik Prescher Date: Wed, 24 Jun 2026 16:39:01 +0200 Subject: [PATCH 63/63] feat: remove agents --- agents | 1 - 1 file changed, 1 deletion(-) delete mode 160000 agents diff --git a/agents b/agents deleted file mode 160000 index a5ed1716a9..0000000000 --- a/agents +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a5ed1716a99d6f4d31be3f3889f6b2e4916b417c