Skip to content

Instantly share code, notes, and snippets.

@czuk
Created October 15, 2025 10:07
Show Gist options
  • Select an option

  • Save czuk/f34242d389d821ea5467d81e7a1a0e33 to your computer and use it in GitHub Desktop.

Select an option

Save czuk/f34242d389d821ea5467d81e7a1a0e33 to your computer and use it in GitHub Desktop.
Document pfSense
<!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