Firewall Policy Analyzer
let currentFmt = 'asa';
// ── Examples ─────────────────────────────────────────────────────────────
const EXAMPLES = {
asa: `access-list OUTSIDE_IN extended permit tcp any host 10.0.0.10 eq 80
access-list OUTSIDE_IN extended permit tcp any host 10.0.0.10 eq 443
access-list OUTSIDE_IN extended permit tcp any host 10.0.0.10 eq 443
access-list OUTSIDE_IN extended permit tcp any 10.0.0.0 255.255.255.0 range 1024 65535
access-list OUTSIDE_IN extended permit tcp any any eq 80
access-list OUTSIDE_IN extended permit tcp 192.168.1.0 255.255.255.0 host 10.0.0.10 eq 80
access-list OUTSIDE_IN extended deny ip any any log`,
ios: `ip access-list extended WEB_FILTER
10 permit tcp 192.168.1.0 0.0.0.255 any eq 80
20 permit tcp 192.168.1.0 0.0.0.255 any eq 443
30 permit tcp any any eq 80
40 permit tcp 192.168.1.0 0.0.0.255 any eq 80
50 permit ip any any
60 deny ip any any log`,
palo: `security {
rules {
"allow-web" {
from [ trust ];
to [ untrust ];
source [ 192.168.1.0/24 ];
destination [ any ];
application [ web-browsing ssl ];
service [ application-default ];
action allow;
}
"allow-any-out" {
from [ trust ];
to [ untrust ];
source [ any ];
destination [ any ];
application [ any ];
service [ any ];
action allow;
}
"block-specific" {
from [ trust ];
to [ untrust ];
source [ 192.168.1.100/32 ];
destination [ any ];
application [ any ];
service [ any ];
action deny;
}
"block-all" {
from [ any ];
to [ any ];
source [ any ];
destination [ any ];
application [ any ];
service [ any ];
action deny;
}
}
}`,
fortigate: `config firewall policy
edit 1
set name "allow-http"
set srcintf "internal"
set dstintf "wan1"
set srcaddr "192.168.1.0/24"
set dstaddr "all"
set action accept
set service "HTTP"
next
edit 2
set name "allow-https"
set srcintf "internal"
set dstintf "wan1"
set srcaddr "192.168.1.0/24"
set dstaddr "all"
set action accept
set service "HTTPS"
next
edit 3
set name "allow-all-out"
set srcintf "internal"
set dstintf "wan1"
set srcaddr "all"
set dstaddr "all"
set action accept
set service "ALL"
next
edit 4
set name "block-shadow"
set srcintf "internal"
set dstintf "wan1"
set srcaddr "192.168.1.50/32"
set dstaddr "all"
set action deny
set service "ALL"
next
end`
};
function setFmt(fmt) {
currentFmt = fmt;
document.querySelectorAll('.format-btn').forEach(b => b.classList.remove('active'));
document.getElementById('fmt-' + fmt).classList.add('active');
}
window.setFmt = setFmt;
function loadExample() {
document.getElementById('policyInput').value = EXAMPLES[currentFmt];
}
window.loadExample = loadExample;
function clearAll() {
document.getElementById('policyInput').value = '';
document.getElementById('resultsDiv').style.display = 'none';
}
window.clearAll = clearAll;
// ── IP helpers ───────────────────────────────────────────────────────────
function ipToInt(ip) {
return ip.split('.').reduce((acc, o) => (acc << 8) | parseInt(o, 10), 0) >>> 0;
}
function prefixMask(len) {
return len === 0 ? 0 : ((~0 << (32 - len)) >>> 0);
}
function wildcardToLen(wildcard) {
const w = ipToInt(wildcard);
const inv = (~w) >>> 0;
let len = 0;
for (let i = 31; i >= 0; i--) {
if (inv & (1 << i)) len++; else break;
}
return len;
}
function cidrToNet(cidr) {
if (!cidr || cidr === 'any' || cidr === 'all') return 'any';
if (cidr.includes('/')) {
const [net, lenStr] = cidr.split('/');
return { net, len: parseInt(lenStr, 10) };
}
return { net: cidr, len: 32 };
}
function parseSrcDst(token) {
if (!token || token === 'any' || token === 'all') return 'any';
return cidrToNet(token);
}
function portSpec(keyword, val) {
if (!keyword || keyword === 'any') return 'any';
if (keyword === 'eq') return { from: parseInt(val, 10), to: parseInt(val, 10) };
if (keyword === 'range') {
const [a, b] = val.split(' ');
return { from: parseInt(a, 10), to: parseInt(b, 10) };
}
const NAMED = { http: 80, https: 443, ssh: 22, telnet: 23, ftp: 21,
smtp: 25, dns: 53, snmp: 161, rdp: 3389, bgp: 179 };
const n = NAMED[keyword.toLowerCase()];
if (n) return { from: n, to: n };
const num = parseInt(keyword, 10);
if (!isNaN(num)) return { from: num, to: num };
return 'any';
}
function serviceToPort(svc) {
if (!svc || svc === 'any' || svc === 'ALL' || svc === 'application-default') return 'any';
const NAMED = { HTTP: 80, HTTPS: 443, SSH: 22, TELNET: 23, FTP: 21,
SMTP: 25, DNS: 53, SNMP: 161, RDP: 3389, BGP: 179 };
const n = NAMED[svc.toUpperCase()];
if (n) return { from: n, to: n };
return 'any';
}
// ── Parsers ───────────────────────────────────────────────────────────────
function parseASA(text) {
const rules = [];
for (const line of text.split('\n')) {
const m = line.trim().match(
/^access-list\s+\S+\s+extended\s+(permit|deny)\s+(\S+)\s+(\S+)(?:\s+(\S+))?\s+(\S+)(?:\s+(eq|range|lt|gt)\s+([\d ]+))?/i
);
if (!m) continue;
const [, action, proto, srcToken, srcWild, dstToken, portKeyword, portVal] = m;
let src = 'any', dst = 'any';
if (srcToken === 'any') {
src = 'any';
if (srcWild === 'host') {
dst = { net: dstToken, len: 32 };
} else {
dst = parseSrcDst(srcWild || dstToken);
}
} else if (srcToken === 'host') {
src = { net: srcWild, len: 32 };
dst = parseSrcDst(dstToken);
} else if (srcToken.match(/^\d/)) {
const nextToken = srcWild || '';
if (nextToken.match(/^\d/)) {
src = { net: srcToken, len: wildcardToLen(nextToken) };
dst = parseSrcDst(dstToken);
} else {
src = cidrToNet(srcToken);
dst = parseSrcDst(srcWild);
}
} else {
src = 'any';
dst = parseSrcDst(srcWild || dstToken);
}
rules.push({
action: action.toLowerCase(),
proto: proto.toLowerCase(),
src, dst,
dstPort: portSpec(portKeyword, portVal),
name: '',
});
}
return rules;
}
function parseIOS(text) {
const rules = [];
for (const line of text.split('\n')) {
const m = line.trim().match(
/^(?:\d+\s+)?(permit|deny)\s+(\S+)\s+(\S+)(?:\s+(\S+))?\s+(\S+)(?:\s+(eq|range|lt|gt)\s+([\d ]+))?/i
);
if (!m) continue;
const [, action, proto, srcToken, srcWild, dstToken, portKeyword, portVal] = m;
if (srcToken.startsWith('access-list')) continue;
let src, dst;
if (srcToken === 'any') {
src = 'any';
dst = parseSrcDst(srcWild || dstToken);
} else if (srcToken === 'host') {
src = { net: srcWild, len: 32 };
dst = parseSrcDst(dstToken);
} else if (srcToken.match(/^\d/)) {
if (srcWild && srcWild.match(/^\d/)) {
src = { net: srcToken, len: wildcardToLen(srcWild) };
dst = parseSrcDst(dstToken);
} else {
src = cidrToNet(srcToken);
dst = parseSrcDst(srcWild);
}
} else {
continue;
}
rules.push({ action: action.toLowerCase(), proto: proto.toLowerCase(), src, dst,
dstPort: portSpec(portKeyword, portVal), name: '' });
}
return rules;
}
function parsePaloAlto(text) {
const rules = [];
const ruleRe = /"([^"]+)"\s*\{([^}]+)\}/gs;
let m;
while ((m = ruleRe.exec(text)) !== null) {
const name = m[1];
const body = m[2];
const get = (key) => {
const rm = body.match(new RegExp(key + '\\s*\\[([^\\]]+)\\]'));
return rm ? rm[1].trim().split(/\s+/) : ['any'];
};
const actionM = body.match(/action\s+(allow|deny|drop)/i);
if (!actionM) continue;
const action = actionM[1].toLowerCase() === 'allow' ? 'permit' : 'deny';
const src = parseSrcDst(get('source')[0]);
const dst = parseSrcDst(get('destination')[0]);
const svcs = get('service');
const dstPort = svcs.length === 1 ? serviceToPort(svcs[0]) : 'any';
rules.push({ action, proto: 'ip', src, dst, dstPort, name });
}
return rules;
}
function parseFortigate(text) {
const rules = [];
const blocks = text.split(/\bedit\s+\d+/).slice(1);
for (const block of blocks) {
const get = (key) => {
const m = block.match(new RegExp('set\\s+' + key + '\\s+"?([^"\\n]+)"?'));
return m ? m[1].trim() : null;
};
const actionRaw = get('action');
if (!actionRaw) continue;
const action = actionRaw === 'accept' ? 'permit' : 'deny';
const srcRaw = get('srcaddr') || 'all';
const dstRaw = get('dstaddr') || 'all';
const svcRaw = get('service') || 'ALL';
const name = get('name') || '';
const src = parseSrcDst(srcRaw === 'all' ? 'any' : srcRaw);
const dst = parseSrcDst(dstRaw === 'all' ? 'any' : dstRaw);
const dstPort = serviceToPort(svcRaw.split(' ')[0]);
rules.push({ action, proto: 'ip', src, dst, dstPort, name });
}
return rules;
}
function parsePolicy(text, fmt) {
if (fmt === 'ios') return parseIOS(text);
if (fmt === 'palo') return parsePaloAlto(text);
if (fmt === 'fortigate') return parseFortigate(text);
return parseASA(text);
}
// ── Shadow / duplicate detection ──────────────────────────────────────────
function networkContains(a, b) {
if (a === 'any') return true;
if (b === 'any') return false;
if (typeof a === 'string') { a = cidrToNet(a); }
if (typeof b === 'string') { b = cidrToNet(b); }
if (a.len > b.len) return false;
const maskA = prefixMask(a.len);
return (ipToInt(a.net) & maskA) === (ipToInt(b.net) & maskA);
}
function portContains(a, b) {
if (a === 'any') return true;
if (b === 'any') return false;
return a.from <= b.from && a.to >= b.to;
}
function protoContains(a, b) {
return a === 'ip' || a === 'any' || a === b;
}
function ruleEquals(a, b) {
return (
a.action === b.action && a.proto === b.proto &&
JSON.stringify(a.src) === JSON.stringify(b.src) &&
JSON.stringify(a.dst) === JSON.stringify(b.dst) &&
JSON.stringify(a.dstPort) === JSON.stringify(b.dstPort)
);
}
function shadows(early, late) {
return (
protoContains(early.proto, late.proto) &&
networkContains(early.src, late.src) &&
networkContains(early.dst, late.dst) &&
portContains(early.dstPort, late.dstPort)
);
}
function isPermissive(rule) {
return rule.src === 'any' && rule.dst === 'any' && rule.dstPort === 'any' && rule.action === 'permit';
}
function detectIssues(rules) {
const issues = rules.map(() => []);
for (let j = 0; j < rules.length; j++) {
if (isPermissive(rules[j]))
issues[j].push({ type: 'perm', msg: 'Permits all source, all destination, all ports' });
for (let i = 0; i < j; i++) {
if (ruleEquals(rules[i], rules[j])) {
issues[j].push({ type: 'dup', msg: `Duplicate of rule ${i + 1}` });
break;
}
if (shadows(rules[i], rules[j])) {
issues[j].push({ type: 'shadow', msg: `Shadowed by rule ${i + 1} — this rule is unreachable` });
break;
}
}
}
return issues;
}
// ── Render ────────────────────────────────────────────────────────────────
function fmtNet(n) {
if (n === 'any') return 'any';
if (typeof n === 'object' && n.len !== undefined)
return n.len === 32 ? n.net : `${n.net}/${n.len}`;
return String(n);
}
function fmtPort(p) {
if (p === 'any') return 'any';
if (p.from === p.to) return String(p.from);
return `${p.from}–${p.to}`;
}
function renderResults(rules, issues) {
const tbody = document.getElementById('policyTableBody');
tbody.innerHTML = '';
let nShadow = 0, nDup = 0, nPerm = 0;
rules.forEach((rule, i) => {
const ruleIssues = issues[i];
const hasShadow = ruleIssues.some(x => x.type === 'shadow');
const hasDup = ruleIssues.some(x => x.type === 'dup');
const hasPerm = ruleIssues.some(x => x.type === 'perm');
if (hasShadow) nShadow++;
if (hasDup) nDup++;
if (hasPerm) nPerm++;
const tr = document.createElement('tr');
tr.className = [
`action-${rule.action}`,
hasShadow ? 'issue-shadow' : hasDup ? 'issue-dup' : hasPerm ? 'issue-perm' : '',
].join(' ').trim();
const badgeHtml = ruleIssues.map(issue => {
const cls = issue.type === 'shadow' ? 'badge-shadow' : issue.type === 'dup' ? 'badge-dup' : 'badge-perm';
const lbl = issue.type === 'shadow' ? 'SHADOWED' : issue.type === 'dup' ? 'DUPLICATE' : 'PERMISSIVE';
return `${lbl}`;
}).join('');
tr.innerHTML = `
${i + 1}
${rule.action.toUpperCase()}
${rule.proto}
${fmtNet(rule.src)}
${fmtNet(rule.dst)}
${fmtPort(rule.dstPort)}
${rule.name || ''}
${badgeHtml} `;
tbody.appendChild(tr);
});
document.getElementById('summaryDiv').innerHTML = `
${rules.length}
Total Rules
${nShadow}
Shadowed
${nDup}
Duplicate
${nPerm}
Permissive
Shadowed: Rule is unreachable — an earlier, broader rule matches all its traffic first. Duplicate: Identical to an earlier rule. Permissive: Permits any source, any destination, all ports.
`; } function analyze() { const text = document.getElementById('policyInput').value.trim(); if (!text) return; const rules = parsePolicy(text, currentFmt); if (rules.length === 0) { alert('No rules recognized. Verify the format matches the selected vendor button, or check for extra whitespace.'); return; } const issues = detectIssues(rules); renderResults(rules, issues); document.getElementById('resultsDiv').style.display = 'block'; } window.analyze = analyze; })();