import { describe, expect, it } from 'bun:test'; import { auditTrustedSources, parseLiveReachRules } from './trusted-sources'; const ZONES = ['10.0.10.0/24', '10.0.20.0/24', '10.0.30.0/24']; const CONTROL_PLANE = '192.168.0.0/24'; const VPN = '10.255.255.0/24'; /** A live `iptables-save -t filter` dump with a hand-added VPN reach rule. */ const LIVE_WITH_UNOWNED = [ '*filter', ':INPUT ACCEPT [0:0]', ':FORWARD DROP [0:0]', ':OUTPUT ACCEPT [0:0]', '-A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT', '-A FORWARD -o eth1 -j ACCEPT', `-A FORWARD -s ${CONTROL_PLANE} -d 10.0.10.0/24 -j ACCEPT`, `-A FORWARD -s ${CONTROL_PLANE} -d 10.0.20.0/24 -j ACCEPT`, `-A FORWARD -s ${CONTROL_PLANE} -d 10.0.30.0/24 -j ACCEPT`, `-A FORWARD -s ${VPN} -d 10.0.10.0/24 -j ACCEPT`, 'COMMIT', ].join('\n'); describe('parsing reach rules from a live ruleset', () => { it('finds subnet-scoped reach into a managed zone', () => { expect(parseLiveReachRules(LIVE_WITH_UNOWNED, ZONES)).toContainEqual({ origin: { kind: 'subnet', value: VPN }, destSubnet: '10.0.10.0/24', }); }); it('finds interface-scoped reach, which celilo cannot model at all', () => { // The shape fw-keeper.sh actually maintained: `-i wg0 -d `. const rules = parseLiveReachRules( '*filter\n-A FORWARD -i wg0 -d 10.0.20.0/24 -j ACCEPT\nCOMMIT', ZONES, ); expect(rules).toEqual([ { origin: { kind: 'interface', value: 'wg0' }, destSubnet: '10.0.20.0/24' }, ]); }); it('ignores rules that do not reach a managed zone', () => { const rules = parseLiveReachRules( [ '*filter', '-A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT', '-A FORWARD -o eth1 -j ACCEPT', '-A FORWARD -s 10.99.0.0/24 -d 172.16.0.0/24 -j ACCEPT', '-A INPUT -s 10.99.0.0/24 -d 10.0.10.0/24 -j ACCEPT', '-A FORWARD -s 10.99.0.0/24 -d 10.0.10.0/24 -j DROP', 'COMMIT', ].join('\n'), ZONES, ); expect(rules).toEqual([]); }); }); describe('unowned-trusted-network audit', () => { it('reports a network celilo does not recognise', async () => { const findings = await auditTrustedSources({ unreachableFirewalls: [], firewalls: [ { firewallIp: '192.168.0.254', live: parseLiveReachRules(LIVE_WITH_UNOWNED, ZONES), known: [CONTROL_PLANE], }, ], }); expect(findings).toHaveLength(1); expect(findings[0].code).toBe('unowned_trusted_network'); expect(findings[0].message).toContain(VPN); expect(findings[0].severity).toBe('drift'); }); it('is silent once that network is registered', async () => { // The point of the change: once a module owns the VPN and registers its // subnet, the rule IS the rendered output and there is nothing to report. const findings = await auditTrustedSources({ unreachableFirewalls: [], firewalls: [ { firewallIp: '192.168.0.254', live: parseLiveReachRules(LIVE_WITH_UNOWNED, ZONES), known: [CONTROL_PLANE, VPN], }, ], }); expect(findings).toEqual([]); }); it('reports an unrecognised network once, not once per zone', async () => { const live = parseLiveReachRules( ZONES.map((z) => `-A FORWARD -s ${VPN} -d ${z} -j ACCEPT`).join('\n'), ZONES, ); const findings = await auditTrustedSources({ unreachableFirewalls: [], firewalls: [{ firewallIp: '192.168.0.254', live, known: [CONTROL_PLANE] }], }); expect(findings).toHaveLength(1); }); it('names the interface when trust is granted by interface, and says why it cannot be registered as-is', async () => { const findings = await auditTrustedSources({ unreachableFirewalls: [], firewalls: [ { firewallIp: '192.168.0.254', live: [{ origin: { kind: 'interface', value: 'wg0' }, destSubnet: '10.0.10.0/24' }], known: [CONTROL_PLANE], }, ], }); expect(findings).toHaveLength(1); expect(findings[0].message).toContain('wg0'); expect(findings[0].details).toContain('SUBNET'); }); it('a firewall with no live reach beyond what celilo composes is clean', async () => { const findings = await auditTrustedSources({ unreachableFirewalls: [], firewalls: [ { firewallIp: '192.168.0.254', live: parseLiveReachRules( ZONES.map((z) => `-A FORWARD -s ${CONTROL_PLANE} -d ${z} -j ACCEPT`).join('\n'), ZONES, ), known: [CONTROL_PLANE], }, ], }); expect(findings).toEqual([]); }); it('a firewall whose ruleset could not be read is unmeasured, not silent (D7)', async () => { const findings = await auditTrustedSources({ unreachableFirewalls: ['192.168.0.254'], firewalls: [], }); expect(findings).toHaveLength(1); expect(findings[0].severity).toBe('unmeasured'); expect(findings[0].code).toBe('trusted_sources_unmeasured'); expect(findings[0].subject).toBe('192.168.0.254'); }); });