Created
October 15, 2025 10:07
-
-
Save czuk/f34242d389d821ea5467d81e7a1a0e33 to your computer and use it in GitHub Desktop.
Document pfSense
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>pfSense Config Parser</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| background: #f5f5f5; | |
| padding: 20px; | |
| line-height: 1.6; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| padding: 30px; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| h1 { | |
| color: #333; | |
| margin-bottom: 20px; | |
| font-size: 28px; | |
| } | |
| .upload-section { | |
| border: 2px dashed #ccc; | |
| padding: 40px; | |
| text-align: center; | |
| border-radius: 8px; | |
| margin-bottom: 30px; | |
| background: #fafafa; | |
| } | |
| .upload-section:hover { | |
| border-color: #999; | |
| background: #f5f5f5; | |
| } | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .upload-btn { | |
| background: #0066cc; | |
| color: white; | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| transition: background 0.3s; | |
| } | |
| .upload-btn:hover { | |
| background: #0052a3; | |
| } | |
| .download-btn { | |
| background: #28a745; | |
| color: white; | |
| padding: 10px 20px; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: background 0.3s; | |
| margin-bottom: 20px; | |
| } | |
| .download-btn:hover { | |
| background: #218838; | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| border-bottom: 2px solid #e0e0e0; | |
| flex-wrap: wrap; | |
| } | |
| .tab { | |
| padding: 10px 20px; | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 14px; | |
| color: #666; | |
| border-bottom: 3px solid transparent; | |
| transition: all 0.3s; | |
| } | |
| .tab:hover { | |
| color: #333; | |
| } | |
| .tab.active { | |
| color: #0066cc; | |
| border-bottom-color: #0066cc; | |
| font-weight: 600; | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .rule { | |
| background: #f9f9f9; | |
| padding: 15px; | |
| margin-bottom: 15px; | |
| border-left: 4px solid #0066cc; | |
| border-radius: 4px; | |
| } | |
| .rule-header { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 8px; | |
| font-size: 16px; | |
| } | |
| .rule-detail { | |
| margin: 5px 0; | |
| color: #555; | |
| font-size: 14px; | |
| } | |
| .rule-detail strong { | |
| color: #333; | |
| min-width: 120px; | |
| display: inline-block; | |
| } | |
| .action-pass { | |
| border-left-color: #28a745; | |
| } | |
| .action-block { | |
| border-left-color: #dc3545; | |
| } | |
| .action-reject { | |
| border-left-color: #ffc107; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 3px 8px; | |
| border-radius: 3px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| margin-left: 10px; | |
| } | |
| .badge-pass { | |
| background: #d4edda; | |
| color: #155724; | |
| } | |
| .badge-block { | |
| background: #f8d7da; | |
| color: #721c24; | |
| } | |
| .badge-reject { | |
| background: #fff3cd; | |
| color: #856404; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px; | |
| color: #999; | |
| font-style: italic; | |
| } | |
| .info-box { | |
| background: #e7f3ff; | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin-bottom: 20px; | |
| border-left: 4px solid #0066cc; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🔒 pfSense Configuration Parser</h1> | |
| <div class="upload-section" id="uploadSection"> | |
| <p style="margin-bottom: 15px; color: #666;">Select your pfSense XML backup file to parse</p> | |
| <input type="file" id="fileInput" accept=".xml"> | |
| <button class="upload-btn" onclick="document.getElementById('fileInput').click()"> | |
| Choose XML File | |
| </button> | |
| <p style="margin-top: 15px; font-size: 13px; color: #999;">Your file is processed locally in your browser - nothing is uploaded</p> | |
| </div> | |
| <div id="results" style="display: none;"> | |
| <button class="download-btn" onclick="exportToExcel()">📥 Download as Excel</button> | |
| <div class="tabs" id="tabs"></div> | |
| <div id="tabContents"></div> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> | |
| <script> | |
| let parsedData = {}; | |
| let interfaceMap = {}; | |
| let aliasMap = {}; | |
| document.getElementById('fileInput').addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const parser = new DOMParser(); | |
| const xmlDoc = parser.parseFromString(e.target.result, "text/xml"); | |
| parseConfig(xmlDoc); | |
| } catch (error) { | |
| alert('Error parsing XML file: ' + error.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }); | |
| function parseConfig(xmlDoc) { | |
| // Build interface and alias maps first | |
| buildInterfaceMap(xmlDoc); | |
| buildAliasMap(xmlDoc); | |
| parsedData = { | |
| firewall: parseFirewallRules(xmlDoc), | |
| nat: parseNATRules(xmlDoc), | |
| aliases: parseAliases(xmlDoc), | |
| interfaces: parseInterfaces(xmlDoc), | |
| vlans: parseVLANs(xmlDoc), | |
| dhcp: parseDHCP(xmlDoc), | |
| system: parseSystem(xmlDoc) | |
| }; | |
| displayResults(); | |
| } | |
| function buildInterfaceMap(xmlDoc) { | |
| interfaceMap = {}; | |
| // Get the interfaces element | |
| const interfacesElement = xmlDoc.querySelector('pfsense > interfaces'); | |
| if (!interfacesElement) return; | |
| // Iterate through each child element (wan, lan, opt1, etc.) | |
| Array.from(interfacesElement.children).forEach((iface) => { | |
| const name = iface.tagName.toLowerCase(); | |
| const descr = getTextContent(iface, 'descr'); | |
| // Only add to map if there's a description, otherwise use the tag name | |
| if (descr && descr.trim() !== '') { | |
| interfaceMap[name] = descr.trim(); | |
| } else { | |
| // Use capitalized version of tag name as fallback | |
| interfaceMap[name] = name.toUpperCase(); | |
| } | |
| }); | |
| } | |
| function buildAliasMap(xmlDoc) { | |
| aliasMap = {}; | |
| const aliasNodes = xmlDoc.querySelectorAll('aliases > alias'); | |
| aliasNodes.forEach((alias) => { | |
| const name = getTextContent(alias, 'name'); | |
| const address = getTextContent(alias, 'address'); | |
| const type = getTextContent(alias, 'type'); | |
| aliasMap[name] = { | |
| address: address, | |
| type: type | |
| }; | |
| }); | |
| } | |
| function getInterfaceName(ifaceKey) { | |
| if (!ifaceKey || ifaceKey === 'any') return 'any'; | |
| // Handle comma-separated list of interfaces | |
| if (ifaceKey.includes(',')) { | |
| const interfaces = ifaceKey.split(',').map(key => { | |
| const trimmed = key.trim().toLowerCase(); | |
| return interfaceMap[trimmed] || key.trim(); | |
| }); | |
| return interfaces.join(', '); | |
| } | |
| const key = ifaceKey.toLowerCase(); | |
| return interfaceMap[key] || ifaceKey; | |
| } | |
| function expandAlias(value) { | |
| if (!value || value === 'any') return value; | |
| // Check if this looks like it might be an alias (no dots, no colons except port) | |
| const parts = value.split(':'); | |
| const addressPart = parts[0]; | |
| const portPart = parts[1]; | |
| if (aliasMap[addressPart]) { | |
| const alias = aliasMap[addressPart]; | |
| const expanded = `${addressPart} (${alias.address})`; | |
| return portPart ? `${expanded}:${portPart}` : expanded; | |
| } | |
| return value; | |
| } | |
| function parseFirewallRules(xmlDoc) { | |
| const rules = []; | |
| const filterRules = xmlDoc.querySelectorAll('filter > rule'); | |
| filterRules.forEach((rule, index) => { | |
| const ifaceKey = getTextContent(rule, 'interface') || 'any'; | |
| const source = parseAddress(rule.querySelector('source')); | |
| const destination = parseAddress(rule.querySelector('destination')); | |
| rules.push({ | |
| id: index + 1, | |
| type: getTextContent(rule, 'type') || 'pass', | |
| interface: ifaceKey, | |
| interfaceName: getInterfaceName(ifaceKey), | |
| ipprotocol: getTextContent(rule, 'ipprotocol') || 'inet', | |
| protocol: getTextContent(rule, 'protocol') || 'any', | |
| source: source, | |
| sourceExpanded: expandAlias(source), | |
| destination: destination, | |
| destinationExpanded: expandAlias(destination), | |
| descr: getTextContent(rule, 'descr') || 'No description', | |
| disabled: rule.querySelector('disabled') ? 'Yes' : 'No', | |
| log: rule.querySelector('log') ? 'Yes' : 'No' | |
| }); | |
| }); | |
| return rules; | |
| } | |
| function parseNATRules(xmlDoc) { | |
| const rules = []; | |
| const natRules = xmlDoc.querySelectorAll('nat > rule'); | |
| natRules.forEach((rule, index) => { | |
| const ifaceKey = getTextContent(rule, 'interface') || 'any'; | |
| const source = parseAddress(rule.querySelector('source')); | |
| const destination = parseAddress(rule.querySelector('destination')); | |
| const target = getTextContent(rule, 'target'); | |
| rules.push({ | |
| id: index + 1, | |
| interface: ifaceKey, | |
| interfaceName: getInterfaceName(ifaceKey), | |
| protocol: getTextContent(rule, 'protocol') || 'any', | |
| source: source, | |
| sourceExpanded: expandAlias(source), | |
| destination: destination, | |
| destinationExpanded: expandAlias(destination), | |
| target: target, | |
| targetExpanded: expandAlias(target), | |
| local_port: getTextContent(rule, 'local-port'), | |
| descr: getTextContent(rule, 'descr') || 'No description', | |
| disabled: rule.querySelector('disabled') ? 'Yes' : 'No' | |
| }); | |
| }); | |
| return rules; | |
| } | |
| function parseAliases(xmlDoc) { | |
| const aliases = []; | |
| const aliasNodes = xmlDoc.querySelectorAll('aliases > alias'); | |
| aliasNodes.forEach((alias, index) => { | |
| aliases.push({ | |
| id: index + 1, | |
| name: getTextContent(alias, 'name'), | |
| type: getTextContent(alias, 'type'), | |
| address: getTextContent(alias, 'address'), | |
| descr: getTextContent(alias, 'descr') || 'No description', | |
| detail: getTextContent(alias, 'detail') | |
| }); | |
| }); | |
| return aliases; | |
| } | |
| function parseInterfaces(xmlDoc) { | |
| const interfaces = []; | |
| const interfaceNodes = xmlDoc.querySelectorAll('interfaces > *'); | |
| interfaceNodes.forEach((iface) => { | |
| const name = iface.tagName; | |
| interfaces.push({ | |
| name: name, | |
| descr: getTextContent(iface, 'descr') || name, | |
| if: getTextContent(iface, 'if'), | |
| ipaddr: getTextContent(iface, 'ipaddr'), | |
| subnet: getTextContent(iface, 'subnet'), | |
| gateway: getTextContent(iface, 'gateway'), | |
| enable: iface.querySelector('enable') ? 'Yes' : 'No' | |
| }); | |
| }); | |
| return interfaces; | |
| } | |
| function parseVLANs(xmlDoc) { | |
| const vlans = []; | |
| const vlanNodes = xmlDoc.querySelectorAll('vlans > vlan'); | |
| vlanNodes.forEach((vlan, index) => { | |
| vlans.push({ | |
| id: index + 1, | |
| if: getTextContent(vlan, 'if'), | |
| tag: getTextContent(vlan, 'tag'), | |
| descr: getTextContent(vlan, 'descr') || 'No description', | |
| vlanif: getTextContent(vlan, 'vlanif') | |
| }); | |
| }); | |
| return vlans; | |
| } | |
| function parseDHCP(xmlDoc) { | |
| const dhcp = []; | |
| const dhcpNodes = xmlDoc.querySelectorAll('dhcpd > *'); | |
| dhcpNodes.forEach((server) => { | |
| const iface = server.tagName; | |
| if (server.querySelector('enable')) { | |
| dhcp.push({ | |
| interface: iface, | |
| range_from: getTextContent(server, 'range > from'), | |
| range_to: getTextContent(server, 'range > to'), | |
| gateway: getTextContent(server, 'gateway'), | |
| domain: getTextContent(server, 'domain'), | |
| dnsserver: Array.from(server.querySelectorAll('dnsserver')).map(dns => dns.textContent).join(', ') | |
| }); | |
| } | |
| }); | |
| return dhcp; | |
| } | |
| function parseSystem(xmlDoc) { | |
| return { | |
| hostname: getTextContent(xmlDoc, 'system > hostname'), | |
| domain: getTextContent(xmlDoc, 'system > domain'), | |
| timezone: getTextContent(xmlDoc, 'system > timezone'), | |
| version: getTextContent(xmlDoc, 'version') | |
| }; | |
| } | |
| function parseAddress(element) { | |
| if (!element) return 'any'; | |
| const network = getTextContent(element, 'network'); | |
| const address = getTextContent(element, 'address'); | |
| const port = getTextContent(element, 'port'); | |
| const any = element.querySelector('any'); | |
| let result = ''; | |
| if (any) { | |
| result = 'any'; | |
| } else if (network) { | |
| result = network; | |
| } else if (address) { | |
| result = address; | |
| } else { | |
| result = 'any'; | |
| } | |
| if (port) { | |
| result += ':' + port; | |
| } | |
| return result; | |
| } | |
| function getTextContent(element, selector) { | |
| if (!element) return ''; | |
| const node = selector.includes('>') ? | |
| element.querySelector(selector) : | |
| element.querySelector(selector); | |
| return node ? node.textContent.trim() : ''; | |
| } | |
| function displayResults() { | |
| const resultsDiv = document.getElementById('results'); | |
| const tabsDiv = document.getElementById('tabs'); | |
| const tabContentsDiv = document.getElementById('tabContents'); | |
| document.getElementById('uploadSection').style.display = 'none'; | |
| resultsDiv.style.display = 'block'; | |
| tabsDiv.innerHTML = ''; | |
| tabContentsDiv.innerHTML = ''; | |
| const sections = [ | |
| { key: 'system', label: 'System Info', icon: '⚙️' }, | |
| { key: 'firewall', label: 'Firewall Rules', icon: '🔥' }, | |
| { key: 'nat', label: 'NAT Rules', icon: '🔀' }, | |
| { key: 'aliases', label: 'Aliases', icon: '📋' }, | |
| { key: 'interfaces', label: 'Interfaces', icon: '🔌' }, | |
| { key: 'vlans', label: 'VLANs', icon: '🏷️' }, | |
| { key: 'dhcp', label: 'DHCP', icon: '📡' } | |
| ]; | |
| sections.forEach((section, index) => { | |
| const data = parsedData[section.key]; | |
| const count = Array.isArray(data) ? data.length : (data ? 1 : 0); | |
| if (count > 0) { | |
| const tab = document.createElement('button'); | |
| tab.className = 'tab' + (index === 0 ? ' active' : ''); | |
| tab.textContent = `${section.icon} ${section.label} (${count})`; | |
| tab.onclick = () => switchTab(section.key); | |
| tabsDiv.appendChild(tab); | |
| const content = document.createElement('div'); | |
| content.id = `tab-${section.key}`; | |
| content.className = 'tab-content' + (index === 0 ? ' active' : ''); | |
| content.innerHTML = renderSection(section.key, data); | |
| tabContentsDiv.appendChild(content); | |
| } | |
| }); | |
| } | |
| function switchTab(key) { | |
| document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); | |
| document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); | |
| event.target.classList.add('active'); | |
| document.getElementById(`tab-${key}`).classList.add('active'); | |
| } | |
| function renderSection(key, data) { | |
| if (key === 'system') { | |
| return ` | |
| <div class="info-box"> | |
| <div class="rule-detail"><strong>Hostname:</strong> ${data.hostname || 'N/A'}</div> | |
| <div class="rule-detail"><strong>Domain:</strong> ${data.domain || 'N/A'}</div> | |
| <div class="rule-detail"><strong>Timezone:</strong> ${data.timezone || 'N/A'}</div> | |
| <div class="rule-detail"><strong>Version:</strong> ${data.version || 'N/A'}</div> | |
| </div> | |
| `; | |
| } | |
| if (key === 'firewall') { | |
| return data.map(rule => { | |
| // Count how many interfaces this rule applies to | |
| const ifaceCount = rule.interface.includes(',') ? rule.interface.split(',').length : 1; | |
| const ifaceDisplay = ifaceCount > 5 ? `${ifaceCount} interfaces` : rule.interfaceName; | |
| return ` | |
| <div class="rule action-${rule.type}"> | |
| <div class="rule-header"> | |
| Rule #${rule.id} - ${ifaceDisplay} | |
| <span class="badge badge-${rule.type}">${rule.type.toUpperCase()}</span> | |
| ${rule.disabled === 'Yes' ? '<span class="badge" style="background:#ccc;color:#666;">DISABLED</span>' : ''} | |
| </div> | |
| ${ifaceCount > 5 ? `<div class="rule-detail"><strong>Interfaces:</strong> ${rule.interfaceName}</div>` : ''} | |
| <div class="rule-detail"><strong>Protocol:</strong> ${rule.protocol} (${rule.ipprotocol})</div> | |
| <div class="rule-detail"><strong>Source:</strong> ${rule.sourceExpanded}</div> | |
| <div class="rule-detail"><strong>Destination:</strong> ${rule.destinationExpanded}</div> | |
| <div class="rule-detail"><strong>Description:</strong> ${rule.descr}</div> | |
| <div class="rule-detail"><strong>Logging:</strong> ${rule.log}</div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| if (key === 'nat') { | |
| return data.map(rule => ` | |
| <div class="rule"> | |
| <div class="rule-header"> | |
| NAT Rule #${rule.id} - ${rule.interfaceName} | |
| ${rule.disabled === 'Yes' ? '<span class="badge" style="background:#ccc;color:#666;">DISABLED</span>' : ''} | |
| </div> | |
| <div class="rule-detail"><strong>Protocol:</strong> ${rule.protocol}</div> | |
| <div class="rule-detail"><strong>Source:</strong> ${rule.sourceExpanded}</div> | |
| <div class="rule-detail"><strong>Destination:</strong> ${rule.destinationExpanded}</div> | |
| ${rule.target ? `<div class="rule-detail"><strong>Target:</strong> ${rule.targetExpanded}</div>` : ''} | |
| ${rule.local_port ? `<div class="rule-detail"><strong>Local Port:</strong> ${rule.local_port}</div>` : ''} | |
| <div class="rule-detail"><strong>Description:</strong> ${rule.descr}</div> | |
| </div> | |
| `).join(''); | |
| } | |
| if (key === 'aliases') { | |
| return data.map(alias => ` | |
| <div class="rule"> | |
| <div class="rule-header">${alias.name}</div> | |
| <div class="rule-detail"><strong>Type:</strong> ${alias.type}</div> | |
| <div class="rule-detail"><strong>Address:</strong> ${alias.address}</div> | |
| <div class="rule-detail"><strong>Description:</strong> ${alias.descr}</div> | |
| ${alias.detail ? `<div class="rule-detail"><strong>Details:</strong> ${alias.detail}</div>` : ''} | |
| </div> | |
| `).join(''); | |
| } | |
| if (key === 'interfaces') { | |
| return data.map(iface => ` | |
| <div class="rule"> | |
| <div class="rule-header"> | |
| ${iface.descr} | |
| ${iface.enable === 'Yes' ? '<span class="badge badge-pass">ENABLED</span>' : '<span class="badge" style="background:#ccc;color:#666;">DISABLED</span>'} | |
| </div> | |
| <div class="rule-detail"><strong>Name:</strong> ${iface.name}</div> | |
| <div class="rule-detail"><strong>Physical:</strong> ${iface.if}</div> | |
| ${iface.ipaddr ? `<div class="rule-detail"><strong>IP Address:</strong> ${iface.ipaddr}${iface.subnet ? '/' + iface.subnet : ''}</div>` : ''} | |
| ${iface.gateway ? `<div class="rule-detail"><strong>Gateway:</strong> ${iface.gateway}</div>` : ''} | |
| </div> | |
| `).join(''); | |
| } | |
| if (key === 'vlans') { | |
| return data.map(vlan => ` | |
| <div class="rule"> | |
| <div class="rule-header">VLAN ${vlan.tag}</div> | |
| <div class="rule-detail"><strong>Interface:</strong> ${vlan.if}</div> | |
| <div class="rule-detail"><strong>Tag:</strong> ${vlan.tag}</div> | |
| <div class="rule-detail"><strong>VLAN IF:</strong> ${vlan.vlanif}</div> | |
| <div class="rule-detail"><strong>Description:</strong> ${vlan.descr}</div> | |
| </div> | |
| `).join(''); | |
| } | |
| if (key === 'dhcp') { | |
| return data.map(server => ` | |
| <div class="rule"> | |
| <div class="rule-header">DHCP Server - ${server.interface}</div> | |
| <div class="rule-detail"><strong>Range:</strong> ${server.range_from} - ${server.range_to}</div> | |
| ${server.gateway ? `<div class="rule-detail"><strong>Gateway:</strong> ${server.gateway}</div>` : ''} | |
| ${server.domain ? `<div class="rule-detail"><strong>Domain:</strong> ${server.domain}</div>` : ''} | |
| ${server.dnsserver ? `<div class="rule-detail"><strong>DNS Servers:</strong> ${server.dnsserver}</div>` : ''} | |
| </div> | |
| `).join(''); | |
| } | |
| return '<div class="empty-state">No data found</div>'; | |
| } | |
| function exportToExcel() { | |
| const workbook = XLSX.utils.book_new(); | |
| // System Info Sheet | |
| if (parsedData.system) { | |
| const systemData = [ | |
| ['Property', 'Value'], | |
| ['Hostname', parsedData.system.hostname || 'N/A'], | |
| ['Domain', parsedData.system.domain || 'N/A'], | |
| ['Timezone', parsedData.system.timezone || 'N/A'], | |
| ['Version', parsedData.system.version || 'N/A'] | |
| ]; | |
| const systemSheet = XLSX.utils.aoa_to_sheet(systemData); | |
| XLSX.utils.book_append_sheet(workbook, systemSheet, 'System Info'); | |
| } | |
| // Firewall Rules Sheet | |
| if (parsedData.firewall && parsedData.firewall.length > 0) { | |
| const firewallData = [ | |
| ['Rule #', 'Action', 'Interface', 'Protocol', 'IP Protocol', 'Source', 'Destination', 'Description', 'Disabled', 'Logging'] | |
| ]; | |
| parsedData.firewall.forEach(rule => { | |
| firewallData.push([ | |
| rule.id, | |
| rule.type, | |
| rule.interfaceName, | |
| rule.protocol, | |
| rule.ipprotocol, | |
| rule.sourceExpanded, | |
| rule.destinationExpanded, | |
| rule.descr, | |
| rule.disabled, | |
| rule.log | |
| ]); | |
| }); | |
| const firewallSheet = XLSX.utils.aoa_to_sheet(firewallData); | |
| XLSX.utils.book_append_sheet(workbook, firewallSheet, 'Firewall Rules'); | |
| } | |
| // NAT Rules Sheet | |
| if (parsedData.nat && parsedData.nat.length > 0) { | |
| const natData = [ | |
| ['Rule #', 'Interface', 'Protocol', 'Source', 'Destination', 'Target', 'Local Port', 'Description', 'Disabled'] | |
| ]; | |
| parsedData.nat.forEach(rule => { | |
| natData.push([ | |
| rule.id, | |
| rule.interfaceName, | |
| rule.protocol, | |
| rule.sourceExpanded, | |
| rule.destinationExpanded, | |
| rule.targetExpanded || '', | |
| rule.local_port || '', | |
| rule.descr, | |
| rule.disabled | |
| ]); | |
| }); | |
| const natSheet = XLSX.utils.aoa_to_sheet(natData); | |
| XLSX.utils.book_append_sheet(workbook, natSheet, 'NAT Rules'); | |
| } | |
| // Aliases Sheet | |
| if (parsedData.aliases && parsedData.aliases.length > 0) { | |
| const aliasData = [ | |
| ['Name', 'Type', 'Address', 'Description', 'Details'] | |
| ]; | |
| parsedData.aliases.forEach(alias => { | |
| aliasData.push([ | |
| alias.name, | |
| alias.type, | |
| alias.address, | |
| alias.descr, | |
| alias.detail || '' | |
| ]); | |
| }); | |
| const aliasSheet = XLSX.utils.aoa_to_sheet(aliasData); | |
| XLSX.utils.book_append_sheet(workbook, aliasSheet, 'Aliases'); | |
| } | |
| // Interfaces Sheet | |
| if (parsedData.interfaces && parsedData.interfaces.length > 0) { | |
| const interfaceData = [ | |
| ['Name', 'Description', 'Physical Interface', 'IP Address', 'Subnet', 'Gateway', 'Enabled'] | |
| ]; | |
| parsedData.interfaces.forEach(iface => { | |
| interfaceData.push([ | |
| iface.name, | |
| iface.descr, | |
| iface.if, | |
| iface.ipaddr || '', | |
| iface.subnet || '', | |
| iface.gateway || '', | |
| iface.enable | |
| ]); | |
| }); | |
| const interfaceSheet = XLSX.utils.aoa_to_sheet(interfaceData); | |
| XLSX.utils.book_append_sheet(workbook, interfaceSheet, 'Interfaces'); | |
| } | |
| // VLANs Sheet | |
| if (parsedData.vlans && parsedData.vlans.length > 0) { | |
| const vlanData = [ | |
| ['Interface', 'VLAN Tag', 'VLAN Interface', 'Description'] | |
| ]; | |
| parsedData.vlans.forEach(vlan => { | |
| vlanData.push([ | |
| vlan.if, | |
| vlan.tag, | |
| vlan.vlanif, | |
| vlan.descr | |
| ]); | |
| }); | |
| const vlanSheet = XLSX.utils.aoa_to_sheet(vlanData); | |
| XLSX.utils.book_append_sheet(workbook, vlanSheet, 'VLANs'); | |
| } | |
| // DHCP Sheet | |
| if (parsedData.dhcp && parsedData.dhcp.length > 0) { | |
| const dhcpData = [ | |
| ['Interface', 'Range From', 'Range To', 'Gateway', 'Domain', 'DNS Servers'] | |
| ]; | |
| parsedData.dhcp.forEach(server => { | |
| dhcpData.push([ | |
| server.interface, | |
| server.range_from, | |
| server.range_to, | |
| server.gateway || '', | |
| server.domain || '', | |
| server.dnsserver || '' | |
| ]); | |
| }); | |
| const dhcpSheet = XLSX.utils.aoa_to_sheet(dhcpData); | |
| XLSX.utils.book_append_sheet(workbook, dhcpSheet, 'DHCP'); | |
| } | |
| // Generate filename with timestamp | |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); | |
| const filename = `pfSense_Config_${parsedData.system?.hostname || 'export'}_${timestamp}.xlsx`; | |
| // Write file | |
| XLSX.writeFile(workbook, filename); | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment