Thursday, April 24, 2025
Fortigate config parser
this python script will take fortigate config file as input with name "fortigate_config.conf" and generate the excel file ipsec_tunnels.xlsx with all the ipsec tunnel details.
save and run the python sccript below:
import re
import pandas as pd
from pathlib import Path
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
def parse_phase1_config(config_text):
"""Parse config vpn ipsec phase1-interface section."""
tunnels = {}
current_tunnel = None
phase1_patterns = {
"Interface": r"set interface \"(\S+)\"",
"Type": r"set type (\S+)",
"Remote Gateway": r"set remote-gw (\S+)",
"Local Gateway": r"set local-gw (\S+)",
"Proposal": r"set proposal (\S+)",
"NAT Traversal": r"set nattraversal (\S+)",
"DPD": r"set dpd (\S+)",
"DPD Retry Count": r"set dpd-retrycount (\d+)",
"DPD Retry Interval": r"set dpd-retryinterval (\d+)",
"Mode": r"set mode (\S+)", # For dial-up (main/aggressive)
"Mode CFG": r"set mode-cfg (\S+)", # For dial-up IP assignment
"Auth User Group": r"set authusrgrp \"(\S+)\"", # For dial-up
"Client IP Range": r"set ipv4-name \"(\S+)\"", # For dial-up
# Note: PSK Secret omitted for security; add if needed
}
phase1_section = re.search(r"config vpn ipsec phase1-interface.*?end", config_text, re.DOTALL)
if not phase1_section:
return tunnels
for line in phase1_section.group(0).splitlines():
line = line.strip()
if line.startswith("edit "):
current_tunnel = line.split('"')[1]
tunnels[current_tunnel] = {}
elif line.startswith("set ") and current_tunnel:
for key, pattern in phase1_patterns.items():
match = re.match(pattern, line)
if match:
tunnels[current_tunnel][key] = match.group(1)
elif line == "next":
current_tunnel = None
# Set defaults for missing fields
for tunnel in tunnels.values():
for key in phase1_patterns:
tunnel.setdefault(key, "N/A")
return tunnels
def parse_phase2_config(config_text):
"""Parse config vpn ipsec phase2-interface section."""
tunnels = {}
current_tunnel = None
phase2_patterns = {
"Phase 1 Name": r"set phase1name \"(\S+)\"",
"Source Subnet": r"set src-subnet (\S+ \S+)",
"Destination Subnet": r"set dst-subnet (\S+ \S+)",
"Source Port": r"set src-port (\d+)",
"Destination Port": r"set dst-port (\d+)",
"Protocol": r"set protocol (\d+)",
"Proposal": r"set proposal (\S+)"
}
phase2_section = re.search(r"config vpn ipsec phase2-interface.*?end", config_text, re.DOTALL)
if not phase2_section:
return tunnels
for line in phase2_section.group(0).splitlines():
line = line.strip()
if line.startswith("edit "):
current_tunnel = line.split('"')[1]
tunnels[current_tunnel] = {}
elif line.startswith("set ") and current_tunnel:
for key, pattern in phase2_patterns.items():
match = re.match(pattern, line)
if match:
tunnels[current_tunnel][key] = match.group(1)
elif line == "next":
current_tunnel = None
# Set defaults for missing fields
for tunnel in tunnels.values():
for key in phase2_patterns:
tunnel.setdefault(key, "N/A")
return tunnels
def parse_firewall_policies(config_text, tunnel_names):
"""Parse config firewall policy section for policies referencing tunnels."""
policies = {name: [] for name in tunnel_names}
current_policy = None
policy_patterns = {
"Policy ID": r"edit (\d+)",
"Source Interface": r"set srcintf \"(\S+)\"",
"Destination Interface": r"set dstintf \"(\S+)\"",
"Source Address": r"set srcaddr \"(\S+)\"",
"Destination Address": r"set dstaddr \"(\S+)\"",
"Service": r"set service \"(\S+)\"",
"Action": r"set action (\S+)",
"Schedule": r"set schedule \"(\S+)\""
}
policy_section = re.search(r"config firewall policy.*?end", config_text, re.DOTALL)
if not policy_section:
return policies
for line in policy_section.group(0).splitlines():
line = line.strip()
if line.startswith("edit "):
current_policy = {"Policy ID": line.split()[1]}
elif line.startswith("set ") and current_policy is not None:
for key, pattern in policy_patterns.items():
match = re.match(pattern, line)
if match:
current_policy[key] = match.group(1)
elif line == "next" and current_policy:
for tunnel_name in tunnel_names:
if (current_policy.get("Source Interface") == tunnel_name or
current_policy.get("Destination Interface") == tunnel_name):
policies[tunnel_name].append(current_policy)
current_policy = None
return policies
def parse_address_objects(config_text):
"""Parse config firewall address section for address objects (e.g., client IP pools)."""
addresses = {}
current_address = None
address_patterns = {
"Type": r"set type (\S+)",
"Start IP": r"set start-ip (\S+)",
"End IP": r"set end-ip (\S+)",
"Subnet": r"set subnet (\S+ \S+)"
}
address_section = re.search(r"config firewall address.*?end", config_text, re.DOTALL)
if not address_section:
return addresses
for line in address_section.group(0).splitlines():
line = line.strip()
if line.startswith("edit "):
current_address = line.split('"')[1]
addresses[current_address] = {}
elif line.startswith("set ") and current_address:
for key, pattern in address_patterns.items():
match = re.match(pattern, line)
if match:
addresses[current_address][key] = match.group(1)
elif line == "next":
current_address = None
return addresses
def create_excel_from_configs(phase1_configs, phase2_configs, firewall_policies, address_objects, output_file="ipsec_tunnels.xlsx"):
"""Create an Excel file from parsed IPsec configurations with an index sheet using openpyxl."""
# Initialize workbook
workbook = Workbook()
# Remove default sheet
default_sheet = workbook.active
workbook.remove(default_sheet)
# Dictionary to store sheet names for index
tunnel_sheets = {}
# Create sheets for each tunnel
for tunnel_name in phase1_configs:
try:
rows = []
# Phase 1 Configuration
for key, value in phase1_configs[tunnel_name].items():
rows.append({"Category": "Phase 1 Configuration", "Field": key, "Value": value})
# Phase 2 Configuration
for phase2_name, phase2_data in phase2_configs.items():
if phase2_data.get("Phase 1 Name") == tunnel_name:
for key, value in phase2_data.items():
rows.append({"Category": f"Phase 2: {phase2_name}", "Field": key, "Value": value})
# Firewall Policies
for policy in firewall_policies.get(tunnel_name, []):
for key, value in policy.items():
rows.append({"Category": "Firewall Policy", "Field": key, "Value": value})
# Address Objects (for dial-up VPNs)
client_ip_range = phase1_configs[tunnel_name].get("Client IP Range", "N/A")
if client_ip_range != "N/A" and client_ip_range in address_objects:
for key, value in address_objects[client_ip_range].items():
rows.append({"Category": "Client IP Range", "Field": key, "Value": value})
# Create DataFrame
df = pd.DataFrame(rows)
# Create sheet with truncated name
sheet_name = tunnel_name[:31] # Excel sheet name limit
worksheet = workbook.create_sheet(sheet_name)
tunnel_sheets[tunnel_name] = sheet_name
# Write DataFrame to worksheet using openpyxl
# Write headers
headers = df.columns.tolist()
for col_idx, header in enumerate(headers, 1):
worksheet[f"{get_column_letter(col_idx)}1"] = header
# Write data
for row_idx, row in enumerate(df.itertuples(index=False), 2):
for col_idx, value in enumerate(row, 1):
worksheet[f"{get_column_letter(col_idx)}{row_idx}"] = str(value)
except Exception as e:
print(f"Error processing tunnel {tunnel_name}: {e}")
# Create index sheet
index_sheet = workbook.create_sheet("Index", 0) # Place at the beginning
index_sheet.append(["Tunnel Name", "Link"])
# Add hyperlinks to each tunnel sheet
row = 2
for tunnel_name, sheet_name in tunnel_sheets.items():
cell = index_sheet[f"A{row}"]
cell.value = tunnel_name
link_cell = index_sheet[f"B{row}"]
# Escape single quotes in sheet name for Excel HYPERLINK formula
escaped_sheet_name = sheet_name.replace("'", "''")
link_cell.value = f'=HYPERLINK("#\'{escaped_sheet_name}\'!A1", "{sheet_name}")'
row += 1
# Save workbook
try:
workbook.save(output_file)
print(f"Excel file created with index sheet: {output_file}")
except Exception as e:
print(f"Error saving Excel file: {e}")
def main():
# Input file
config_file = "fortigate_config.conf" # Replace with your config file
output_file = "ipsec_tunnels.xlsx"
try:
# Read config file
with open(config_file, "r") as f:
config_text = f.read()
# Parse configurations
phase1_configs = parse_phase1_config(config_text)
phase2_configs = parse_phase2_config(config_text)
firewall_policies = parse_firewall_policies(config_text, phase1_configs.keys())
address_objects = parse_address_objects(config_text)
if not phase1_configs:
print("No IPsec tunnels found in the configuration file.")
return
# Create Excel with index sheet
create_excel_from_configs(phase1_configs, phase2_configs, firewall_policies, address_objects, output_file)
except FileNotFoundError:
print(f"Config file {config_file} not found.")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
Subscribe to:
Posts (Atom)