防火牆策略分析器

貼上 Cisco ASA、Cisco IOS ACL、Palo Alto 或 Fortigate 的策略輸出,可偵測到隱藏規則、重複或過於寬鬆的條目 — 完全在您的瀏覽器中處理。

輸入
供應商格式:
(function () { 'use strict'; 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; })();